a tool for shared writing and social publishing
1import { PgTransaction } from "drizzle-orm/pg-core";
2import * as driz from "drizzle-orm";
3import * as base64 from "base64-js";
4import * as Y from "yjs";
5import { MutationContext } from "./mutations";
6import { entities, facts } from "drizzle/schema";
7import { Attribute, Attributes, FilterAttributes } from "./attributes";
8import { Fact, PermissionToken } from ".";
9import { DeepReadonly } from "replicache";
10import { createClient } from "@supabase/supabase-js";
11import { Database } from "supabase/database.types";
12import { v7 } from "uuid";
13export function serverMutationContext(
14 tx: PgTransaction<any, any, any>,
15 permission_token_id: string,
16 token_rights: PermissionToken["permission_token_rights"],
17) {
18 let ctx: MutationContext & {
19 checkPermission: (entity: string) => Promise<boolean>;
20 } = {
21 permission_token_id,
22 async runOnServer(cb) {
23 let supabase = createClient<Database>(
24 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
25 process.env.SUPABASE_SERVICE_ROLE_KEY as string,
26 );
27 return cb({ supabase });
28 },
29 async checkPermission(entity: string) {
30 let [permission_set] = await tx
31 .select({ entity_set: entities.set })
32 .from(entities)
33 .where(driz.eq(entities.id, entity));
34 return (
35 !!permission_set &&
36 !!token_rights.find(
37 (r) => r.entity_set === permission_set.entity_set && r.write == true,
38 )
39 );
40 },
41 async runOnClient(_cb) {},
42 async createEntity({ entityID, permission_set }) {
43 if (
44 !token_rights.find(
45 (r) => r.entity_set === permission_set && r.write === true,
46 )
47 ) {
48 return false;
49 }
50 await tx.transaction(
51 async (tx2) =>
52 await tx2
53 .insert(entities)
54 .values({
55 set: permission_set,
56 id: entityID,
57 })
58 .catch(console.log),
59 );
60 return true;
61 },
62 scanIndex: {
63 async eav(entity, attribute) {
64 return (await tx
65 .select({
66 id: facts.id,
67 data: facts.data,
68 entity: facts.entity,
69 attribute: facts.attribute,
70 })
71 .from(facts)
72 .where(
73 driz.and(
74 driz.eq(facts.attribute, attribute),
75 driz.eq(facts.entity, entity),
76 ),
77 )) as DeepReadonly<Fact<typeof attribute>>[];
78 },
79 },
80 async assertFact(f) {
81 if (!f.entity) return;
82 let attribute = Attributes[f.attribute as Attribute];
83 if (!attribute) return;
84 let id = f.id || v7();
85 let data = { ...f.data };
86 let [permission_set] = await tx
87 .select({ entity_set: entities.set })
88 .from(entities)
89 .where(driz.eq(entities.id, f.entity));
90 if (!(await this.checkPermission(f.entity))) return;
91 if (attribute.cardinality === "one") {
92 let existingFact = await tx
93 .select({ id: facts.id, data: facts.data })
94 .from(facts)
95 .where(
96 driz.and(
97 driz.eq(facts.attribute, f.attribute),
98 driz.eq(facts.entity, f.entity),
99 ),
100 );
101 if (existingFact[0]) {
102 id = existingFact[0].id;
103 if (attribute.type === "text") {
104 const oldUpdate = base64.toByteArray(
105 (
106 existingFact[0]?.data as Fact<
107 keyof FilterAttributes<{ type: "text" }>
108 >["data"]
109 ).value,
110 );
111
112 let textData = data as Fact<
113 keyof FilterAttributes<{ type: "text" }>
114 >["data"];
115 const newUpdate = base64.toByteArray(textData.value);
116 const updateBytes = Y.mergeUpdates([oldUpdate, newUpdate]);
117 textData.value = base64.fromByteArray(updateBytes);
118 }
119 }
120 }
121 await tx.transaction(
122 async (tx2) =>
123 await tx2
124 .insert(facts)
125 .values({
126 id: id,
127 entity: f.entity,
128 data: driz.sql`${data}::jsonb`,
129 attribute: f.attribute,
130 })
131 .onConflictDoUpdate({
132 target: facts.id,
133 set: { data: driz.sql`${f.data}::jsonb` },
134 })
135 .catch((e) => {
136 console.log(`error on inserting fact: `, JSON.stringify(e));
137 }),
138 );
139 },
140 async retractFact(id) {
141 let [f] = await tx
142 .select()
143 .from(facts)
144 .rightJoin(entities, driz.eq(entities.id, facts.entity))
145 .where(driz.eq(facts.id, id));
146 if (!f || !(await this.checkPermission(f.entities.id))) return;
147 await tx.delete(facts).where(driz.eq(facts.id, id));
148 },
149 async deleteEntity(entity) {
150 if (!(await this.checkPermission(entity))) return;
151 await Promise.all([
152 tx.delete(entities).where(driz.eq(entities.id, entity)),
153 tx
154 .delete(facts)
155 .where(
156 driz.sql`(data->>'type' = 'ordered-reference' or data ->>'type' = 'reference' or data ->>'type' = 'spatial-reference') and data->>'value' = ${entity}`,
157 ),
158 ]);
159 },
160 };
161 return ctx;
162}