a tool for shared writing and social publishing

use supabase rpc for pull

+166 -103
+34 -42
app/api/rpc/[command]/pull.ts
··· 4 4 PullResponseV1, 5 5 VersionNotSupportedResponse, 6 6 } from "replicache"; 7 - import { Database } from "supabase/database.types"; 8 7 import { Fact } from "src/replicache"; 9 - import postgres from "postgres"; 10 - import { drizzle } from "drizzle-orm/postgres-js"; 11 - import { FactWithIndexes, getClientGroup } from "src/replicache/utils"; 8 + import { FactWithIndexes } from "src/replicache/utils"; 12 9 import { Attributes } from "src/replicache/attributes"; 13 - import { permission_tokens } from "drizzle/schema"; 14 - import { eq, sql } from "drizzle-orm"; 15 10 import { makeRoute } from "../lib"; 16 11 import { Env } from "./route"; 17 12 ··· 52 47 route: "pull", 53 48 input: z.object({ pullRequest: PullRequestSchema, token_id: z.string() }), 54 49 handler: async ({ pullRequest, token_id }, { supabase }: Env) => { 55 - const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 56 - const db = drizzle(client); 57 50 let body = pullRequest; 58 51 if (body.pullVersion === 0) return versionNotSupported; 59 - let [facts, clientGroup] = await db.transaction(async (tx) => { 60 - let [token] = await tx 61 - .select({ root_entity: permission_tokens.root_entity }) 62 - .from(permission_tokens) 63 - .where(eq(permission_tokens.id, token_id)); 52 + let { data, error } = await supabase.rpc("pull_data", { 53 + token_id, 54 + client_group_id: body.clientGroupID, 55 + }); 56 + if (!data) { 57 + console.log(error); 64 58 65 - let facts: { 66 - attribute: string; 67 - created_at: string; 68 - data: any; 69 - entity: string; 70 - id: string; 71 - updated_at: string | null; 72 - version: number; 73 - }[] = []; 74 - let clientGroup = {}; 59 + return { 60 + error: "ClientStateNotFound", 61 + } as const; 62 + } 75 63 76 - if (token) { 77 - let data = (await tx.execute( 78 - sql`select * from get_facts(${token.root_entity}) as get_facts`, 79 - )) as { 80 - attribute: string; 81 - created_at: string; 82 - data: any; 83 - entity: string; 84 - id: string; 85 - updated_at: string | null; 86 - version: number; 87 - }[]; 64 + let facts = data.facts as { 65 + attribute: string; 66 + created_at: string; 67 + data: any; 68 + entity: string; 69 + id: string; 70 + updated_at: string | null; 71 + version: number; 72 + }[]; 88 73 89 - clientGroup = await getClientGroup(tx, body.clientGroupID); 90 - facts = data || []; 91 - return [facts, clientGroup]; 92 - } 93 - return []; 94 - }); 95 - client.end(); 74 + let clientGroup = ( 75 + (data.client_groups as { 76 + client_id: string; 77 + client_group: string; 78 + last_mutation: number; 79 + }[]) || [] 80 + ).reduce( 81 + (acc, clientRecord) => { 82 + acc[clientRecord.client_id] = clientRecord.last_mutation; 83 + return acc; 84 + }, 85 + {} as { [clientID: string]: number }, 86 + ); 87 + 96 88 return { 97 89 cookie: Date.now(), 98 90 lastMutationIDChanges: clientGroup,
+36 -36
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { entities, poll_votes_on_entity, entity_sets, facts, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, permission_token_on_homepage, permission_token_rights } from "./schema"; 2 + import { entities, facts, entity_sets, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, poll_votes_on_entity, permission_token_on_homepage, permission_token_rights } from "./schema"; 3 3 4 - export const poll_votes_on_entityRelations = relations(poll_votes_on_entity, ({one}) => ({ 5 - entity_option_entity: one(entities, { 6 - fields: [poll_votes_on_entity.option_entity], 7 - references: [entities.id], 8 - relationName: "poll_votes_on_entity_option_entity_entities_id" 9 - }), 10 - entity_poll_entity: one(entities, { 11 - fields: [poll_votes_on_entity.poll_entity], 12 - references: [entities.id], 13 - relationName: "poll_votes_on_entity_poll_entity_entities_id" 4 + export const factsRelations = relations(facts, ({one}) => ({ 5 + entity: one(entities, { 6 + fields: [facts.entity], 7 + references: [entities.id] 14 8 }), 15 9 })); 16 10 17 11 export const entitiesRelations = relations(entities, ({one, many}) => ({ 18 - poll_votes_on_entities_option_entity: many(poll_votes_on_entity, { 19 - relationName: "poll_votes_on_entity_option_entity_entities_id" 20 - }), 21 - poll_votes_on_entities_poll_entity: many(poll_votes_on_entity, { 22 - relationName: "poll_votes_on_entity_poll_entity_entities_id" 23 - }), 12 + facts: many(facts), 24 13 entity_set: one(entity_sets, { 25 14 fields: [entities.set], 26 15 references: [entity_sets.id] 27 16 }), 28 - facts: many(facts), 29 17 permission_tokens: many(permission_tokens), 30 18 email_subscriptions_to_entities: many(email_subscriptions_to_entity), 31 19 phone_rsvps_to_entities: many(phone_rsvps_to_entity), 20 + poll_votes_on_entities_option_entity: many(poll_votes_on_entity, { 21 + relationName: "poll_votes_on_entity_option_entity_entities_id" 22 + }), 23 + poll_votes_on_entities_poll_entity: many(poll_votes_on_entity, { 24 + relationName: "poll_votes_on_entity_poll_entity_entities_id" 25 + }), 32 26 })); 33 27 34 28 export const entity_setsRelations = relations(entity_sets, ({many}) => ({ ··· 36 30 permission_token_rights: many(permission_token_rights), 37 31 })); 38 32 39 - export const factsRelations = relations(facts, ({one}) => ({ 40 - entity: one(entities, { 41 - fields: [facts.entity], 42 - references: [entities.id] 43 - }), 44 - })); 45 - 46 - export const identitiesRelations = relations(identities, ({one, many}) => ({ 47 - permission_token: one(permission_tokens, { 48 - fields: [identities.home_page], 49 - references: [permission_tokens.id] 50 - }), 51 - email_auth_tokens: many(email_auth_tokens), 52 - custom_domains: many(custom_domains), 53 - permission_token_on_homepages: many(permission_token_on_homepage), 54 - })); 55 - 56 33 export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 57 - identities: many(identities), 58 34 entity: one(entities, { 59 35 fields: [permission_tokens.root_entity], 60 36 references: [entities.id] 61 37 }), 38 + identities: many(identities), 62 39 email_subscriptions_to_entities: many(email_subscriptions_to_entity), 63 40 custom_domain_routes_edit_permission_token: many(custom_domain_routes, { 64 41 relationName: "custom_domain_routes_edit_permission_token_permission_tokens_id" ··· 68 45 }), 69 46 permission_token_on_homepages: many(permission_token_on_homepage), 70 47 permission_token_rights: many(permission_token_rights), 48 + })); 49 + 50 + export const identitiesRelations = relations(identities, ({one, many}) => ({ 51 + permission_token: one(permission_tokens, { 52 + fields: [identities.home_page], 53 + references: [permission_tokens.id] 54 + }), 55 + email_auth_tokens: many(email_auth_tokens), 56 + custom_domains: many(custom_domains), 57 + permission_token_on_homepages: many(permission_token_on_homepage), 71 58 })); 72 59 73 60 export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ ··· 117 104 identity: one(identities, { 118 105 fields: [custom_domains.identity], 119 106 references: [identities.email] 107 + }), 108 + })); 109 + 110 + export const poll_votes_on_entityRelations = relations(poll_votes_on_entity, ({one}) => ({ 111 + entity_option_entity: one(entities, { 112 + fields: [poll_votes_on_entity.option_entity], 113 + references: [entities.id], 114 + relationName: "poll_votes_on_entity_option_entity_entities_id" 115 + }), 116 + entity_poll_entity: one(entities, { 117 + fields: [poll_votes_on_entity.poll_entity], 118 + references: [entities.id], 119 + relationName: "poll_votes_on_entity_poll_entity_entities_id" 120 120 }), 121 121 })); 122 122
+21 -20
drizzle/schema.ts
··· 1 - import { pgTable, foreignKey, pgEnum, uuid, timestamp, text, jsonb, bigint, unique, boolean, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 1 + import { pgTable, foreignKey, pgEnum, uuid, text, jsonb, timestamp, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 14 14 export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 15 15 16 16 17 - export const poll_votes_on_entity = pgTable("poll_votes_on_entity", { 18 - id: uuid("id").defaultRandom().primaryKey().notNull(), 19 - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 20 - poll_entity: uuid("poll_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 21 - option_entity: uuid("option_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 22 - voter_token: uuid("voter_token").notNull(), 23 - }); 24 - 25 - export const entities = pgTable("entities", { 26 - id: uuid("id").primaryKey().notNull(), 27 - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 28 - set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 29 - }); 30 - 31 17 export const facts = pgTable("facts", { 32 18 id: uuid("id").primaryKey().notNull(), 33 19 entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), ··· 46 32 last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 47 33 }); 48 34 35 + export const entities = pgTable("entities", { 36 + id: uuid("id").primaryKey().notNull(), 37 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 38 + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 39 + }); 40 + 49 41 export const entity_sets = pgTable("entity_sets", { 50 42 id: uuid("id").defaultRandom().primaryKey().notNull(), 51 43 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 44 + }); 45 + 46 + export const permission_tokens = pgTable("permission_tokens", { 47 + id: uuid("id").defaultRandom().primaryKey().notNull(), 48 + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 49 + blocked_by_admin: boolean("blocked_by_admin"), 52 50 }); 53 51 54 52 export const identities = pgTable("identities", { ··· 61 59 return { 62 60 identities_email_key: unique("identities_email_key").on(table.email), 63 61 } 64 - }); 65 - 66 - export const permission_tokens = pgTable("permission_tokens", { 67 - id: uuid("id").defaultRandom().primaryKey().notNull(), 68 - root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 69 62 }); 70 63 71 64 export const email_subscriptions_to_entity = pgTable("email_subscriptions_to_entity", { ··· 131 124 identity: text("identity").default('').notNull().references(() => identities.email, { onDelete: "cascade", onUpdate: "cascade" } ), 132 125 confirmed: boolean("confirmed").notNull(), 133 126 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 127 + }); 128 + 129 + export const poll_votes_on_entity = pgTable("poll_votes_on_entity", { 130 + id: uuid("id").defaultRandom().primaryKey().notNull(), 131 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 132 + poll_entity: uuid("poll_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 133 + option_entity: uuid("option_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 134 + voter_token: uuid("voter_token").notNull(), 134 135 }); 135 136 136 137 export const permission_token_on_homepage = pgTable("permission_token_on_homepage", {
+50 -5
supabase/database.types.ts
··· 472 472 }, 473 473 ] 474 474 } 475 + poll_votes_on_entity: { 476 + Row: { 477 + created_at: string 478 + id: string 479 + option_entity: string 480 + poll_entity: string 481 + voter_token: string 482 + } 483 + Insert: { 484 + created_at?: string 485 + id?: string 486 + option_entity: string 487 + poll_entity: string 488 + voter_token: string 489 + } 490 + Update: { 491 + created_at?: string 492 + id?: string 493 + option_entity?: string 494 + poll_entity?: string 495 + voter_token?: string 496 + } 497 + Relationships: [ 498 + { 499 + foreignKeyName: "poll_votes_on_entity_option_entity_fkey" 500 + columns: ["option_entity"] 501 + isOneToOne: false 502 + referencedRelation: "entities" 503 + referencedColumns: ["id"] 504 + }, 505 + { 506 + foreignKeyName: "poll_votes_on_entity_poll_entity_fkey" 507 + columns: ["poll_entity"] 508 + isOneToOne: false 509 + referencedRelation: "entities" 510 + referencedColumns: ["id"] 511 + }, 512 + ] 513 + } 475 514 replicache_clients: { 476 515 Row: { 477 516 client_group: string ··· 534 573 like: unknown 535 574 }[] 536 575 } 576 + pull_data: { 577 + Args: { 578 + token_id: string 579 + client_group_id: string 580 + } 581 + Returns: Database["public"]["CompositeTypes"]["pull_result"] 582 + } 537 583 } 538 584 Enums: { 539 585 rsvp_status: "GOING" | "NOT_GOING" | "MAYBE" 540 586 } 541 587 CompositeTypes: { 542 - [_ in never]: never 588 + pull_result: { 589 + client_groups: Json | null 590 + facts: Json | null 591 + } 543 592 } 544 593 } 545 594 storage: { ··· 818 867 metadata: Json 819 868 updated_at: string 820 869 }[] 821 - } 822 - operation: { 823 - Args: Record<PropertyKey, never> 824 - Returns: string 825 870 } 826 871 search: { 827 872 Args: {
+25
supabase/migrations/20250305223244_add_pull_rpc_function.sql
··· 1 + create type "public"."pull_result" as ("client_groups" json, "facts" json); 2 + CREATE OR REPLACE FUNCTION public.pull_data(token_id uuid, client_group_id text) 3 + RETURNS pull_result 4 + LANGUAGE plpgsql 5 + AS $function$ 6 + DECLARE 7 + result pull_result; 8 + BEGIN 9 + -- Get client group data as JSON array 10 + SELECT json_agg(row_to_json(rc)) 11 + FROM replicache_clients rc 12 + WHERE rc.client_group = client_group_id 13 + INTO result.client_groups; 14 + 15 + -- Get facts as JSON array 16 + SELECT json_agg(row_to_json(f)) 17 + FROM permission_tokens pt, 18 + get_facts(pt.root_entity) f 19 + WHERE pt.id = token_id 20 + INTO result.facts; 21 + 22 + RETURN result; 23 + END; 24 + $function$ 25 + ;