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}