diff --git a/app/api/rpc/[command]/pull.ts b/app/api/rpc/[command]/pull.ts index 60e6dfff..409f2e5f 100644 --- a/app/api/rpc/[command]/pull.ts +++ b/app/api/rpc/[command]/pull.ts @@ -4,14 +4,9 @@ import { PullResponseV1, VersionNotSupportedResponse, } from "replicache"; -import { Database } from "supabase/database.types"; import { Fact } from "src/replicache"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; -import { FactWithIndexes, getClientGroup } from "src/replicache/utils"; +import { FactWithIndexes } from "src/replicache/utils"; import { Attributes } from "src/replicache/attributes"; -import { permission_tokens } from "drizzle/schema"; -import { eq, sql } from "drizzle-orm"; import { makeRoute } from "../lib"; import { Env } from "./route"; @@ -52,47 +47,44 @@ export const pull = makeRoute({ route: "pull", input: z.object({ pullRequest: PullRequestSchema, token_id: z.string() }), handler: async ({ pullRequest, token_id }, { supabase }: Env) => { - const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); - const db = drizzle(client); let body = pullRequest; if (body.pullVersion === 0) return versionNotSupported; - let [facts, clientGroup] = await db.transaction(async (tx) => { - let [token] = await tx - .select({ root_entity: permission_tokens.root_entity }) - .from(permission_tokens) - .where(eq(permission_tokens.id, token_id)); + let { data, error } = await supabase.rpc("pull_data", { + token_id, + client_group_id: body.clientGroupID, + }); + if (!data) { + console.log(error); - let facts: { - attribute: string; - created_at: string; - data: any; - entity: string; - id: string; - updated_at: string | null; - version: number; - }[] = []; - let clientGroup = {}; + return { + error: "ClientStateNotFound", + } as const; + } - if (token) { - let data = (await tx.execute( - sql`select * from get_facts(${token.root_entity}) as get_facts`, - )) as { - attribute: string; - created_at: string; - data: any; - entity: string; - id: string; - updated_at: string | null; - version: number; - }[]; + let facts = data.facts as { + attribute: string; + created_at: string; + data: any; + entity: string; + id: string; + updated_at: string | null; + version: number; + }[]; + + let clientGroup = ( + (data.client_groups as { + client_id: string; + client_group: string; + last_mutation: number; + }[]) || [] + ).reduce( + (acc, clientRecord) => { + acc[clientRecord.client_id] = clientRecord.last_mutation; + return acc; + }, + {} as { [clientID: string]: number }, + ); - clientGroup = await getClientGroup(tx, body.clientGroupID); - facts = data || []; - return [facts, clientGroup]; - } - return []; - }); - client.end(); return { cookie: Date.now(), lastMutationIDChanges: clientGroup, diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 9ff3dbd9..ad788354 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,34 +1,28 @@ import { relations } from "drizzle-orm/relations"; -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"; +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"; -export const poll_votes_on_entityRelations = relations(poll_votes_on_entity, ({one}) => ({ - entity_option_entity: one(entities, { - fields: [poll_votes_on_entity.option_entity], - references: [entities.id], - relationName: "poll_votes_on_entity_option_entity_entities_id" - }), - entity_poll_entity: one(entities, { - fields: [poll_votes_on_entity.poll_entity], - references: [entities.id], - relationName: "poll_votes_on_entity_poll_entity_entities_id" +export const factsRelations = relations(facts, ({one}) => ({ + entity: one(entities, { + fields: [facts.entity], + references: [entities.id] }), })); export const entitiesRelations = relations(entities, ({one, many}) => ({ - poll_votes_on_entities_option_entity: many(poll_votes_on_entity, { - relationName: "poll_votes_on_entity_option_entity_entities_id" - }), - poll_votes_on_entities_poll_entity: many(poll_votes_on_entity, { - relationName: "poll_votes_on_entity_poll_entity_entities_id" - }), + facts: many(facts), entity_set: one(entity_sets, { fields: [entities.set], references: [entity_sets.id] }), - facts: many(facts), permission_tokens: many(permission_tokens), email_subscriptions_to_entities: many(email_subscriptions_to_entity), phone_rsvps_to_entities: many(phone_rsvps_to_entity), + poll_votes_on_entities_option_entity: many(poll_votes_on_entity, { + relationName: "poll_votes_on_entity_option_entity_entities_id" + }), + poll_votes_on_entities_poll_entity: many(poll_votes_on_entity, { + relationName: "poll_votes_on_entity_poll_entity_entities_id" + }), })); export const entity_setsRelations = relations(entity_sets, ({many}) => ({ @@ -36,29 +30,12 @@ export const entity_setsRelations = relations(entity_sets, ({many}) => ({ permission_token_rights: many(permission_token_rights), })); -export const factsRelations = relations(facts, ({one}) => ({ - entity: one(entities, { - fields: [facts.entity], - references: [entities.id] - }), -})); - -export const identitiesRelations = relations(identities, ({one, many}) => ({ - permission_token: one(permission_tokens, { - fields: [identities.home_page], - references: [permission_tokens.id] - }), - email_auth_tokens: many(email_auth_tokens), - custom_domains: many(custom_domains), - permission_token_on_homepages: many(permission_token_on_homepage), -})); - export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ - identities: many(identities), entity: one(entities, { fields: [permission_tokens.root_entity], references: [entities.id] }), + identities: many(identities), email_subscriptions_to_entities: many(email_subscriptions_to_entity), custom_domain_routes_edit_permission_token: many(custom_domain_routes, { relationName: "custom_domain_routes_edit_permission_token_permission_tokens_id" @@ -70,6 +47,16 @@ export const permission_tokensRelations = relations(permission_tokens, ({one, ma permission_token_rights: many(permission_token_rights), })); +export const identitiesRelations = relations(identities, ({one, many}) => ({ + permission_token: one(permission_tokens, { + fields: [identities.home_page], + references: [permission_tokens.id] + }), + email_auth_tokens: many(email_auth_tokens), + custom_domains: many(custom_domains), + permission_token_on_homepages: many(permission_token_on_homepage), +})); + export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ entity: one(entities, { fields: [email_subscriptions_to_entity.entity], @@ -120,6 +107,19 @@ export const custom_domainsRelations = relations(custom_domains, ({one, many}) = }), })); +export const poll_votes_on_entityRelations = relations(poll_votes_on_entity, ({one}) => ({ + entity_option_entity: one(entities, { + fields: [poll_votes_on_entity.option_entity], + references: [entities.id], + relationName: "poll_votes_on_entity_option_entity_entities_id" + }), + entity_poll_entity: one(entities, { + fields: [poll_votes_on_entity.poll_entity], + references: [entities.id], + relationName: "poll_votes_on_entity_poll_entity_entities_id" + }), +})); + export const permission_token_on_homepageRelations = relations(permission_token_on_homepage, ({one}) => ({ identity: one(identities, { fields: [permission_token_on_homepage.identity], diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 94234ea4..f7a7327a 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, foreignKey, pgEnum, uuid, timestamp, text, jsonb, bigint, unique, boolean, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" +import { pgTable, foreignKey, pgEnum, uuid, text, jsonb, timestamp, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) @@ -14,20 +14,6 @@ export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE' export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) -export const poll_votes_on_entity = pgTable("poll_votes_on_entity", { - id: uuid("id").defaultRandom().primaryKey().notNull(), - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), - poll_entity: uuid("poll_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), - option_entity: uuid("option_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), - voter_token: uuid("voter_token").notNull(), -}); - -export const entities = pgTable("entities", { - id: uuid("id").primaryKey().notNull(), - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), - set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), -}); - export const facts = pgTable("facts", { id: uuid("id").primaryKey().notNull(), entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), @@ -46,11 +32,23 @@ export const replicache_clients = pgTable("replicache_clients", { last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), }); +export const entities = pgTable("entities", { + id: uuid("id").primaryKey().notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + export const entity_sets = pgTable("entity_sets", { id: uuid("id").defaultRandom().primaryKey().notNull(), created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), }); +export const permission_tokens = pgTable("permission_tokens", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), + blocked_by_admin: boolean("blocked_by_admin"), +}); + export const identities = pgTable("identities", { id: uuid("id").defaultRandom().primaryKey().notNull(), created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), @@ -63,11 +61,6 @@ export const identities = pgTable("identities", { } }); -export const permission_tokens = pgTable("permission_tokens", { - id: uuid("id").defaultRandom().primaryKey().notNull(), - root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), -}); - export const email_subscriptions_to_entity = pgTable("email_subscriptions_to_entity", { id: uuid("id").defaultRandom().primaryKey().notNull(), entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade" } ), @@ -133,6 +126,14 @@ export const custom_domains = pgTable("custom_domains", { created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), }); +export const poll_votes_on_entity = pgTable("poll_votes_on_entity", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + poll_entity: uuid("poll_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), + option_entity: uuid("option_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), + voter_token: uuid("voter_token").notNull(), +}); + export const permission_token_on_homepage = pgTable("permission_token_on_homepage", { token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), identity: uuid("identity").notNull().references(() => identities.id, { onDelete: "cascade" } ), diff --git a/supabase/database.types.ts b/supabase/database.types.ts index e96ca834..61a2f459 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -472,6 +472,45 @@ export type Database = { }, ] } + poll_votes_on_entity: { + Row: { + created_at: string + id: string + option_entity: string + poll_entity: string + voter_token: string + } + Insert: { + created_at?: string + id?: string + option_entity: string + poll_entity: string + voter_token: string + } + Update: { + created_at?: string + id?: string + option_entity?: string + poll_entity?: string + voter_token?: string + } + Relationships: [ + { + foreignKeyName: "poll_votes_on_entity_option_entity_fkey" + columns: ["option_entity"] + isOneToOne: false + referencedRelation: "entities" + referencedColumns: ["id"] + }, + { + foreignKeyName: "poll_votes_on_entity_poll_entity_fkey" + columns: ["poll_entity"] + isOneToOne: false + referencedRelation: "entities" + referencedColumns: ["id"] + }, + ] + } replicache_clients: { Row: { client_group: string @@ -534,12 +573,22 @@ export type Database = { like: unknown }[] } + pull_data: { + Args: { + token_id: string + client_group_id: string + } + Returns: Database["public"]["CompositeTypes"]["pull_result"] + } } Enums: { rsvp_status: "GOING" | "NOT_GOING" | "MAYBE" } CompositeTypes: { - [_ in never]: never + pull_result: { + client_groups: Json | null + facts: Json | null + } } } storage: { @@ -819,10 +868,6 @@ export type Database = { updated_at: string }[] } - operation: { - Args: Record - Returns: string - } search: { Args: { prefix: string diff --git a/supabase/migrations/20250305223244_add_pull_rpc_function.sql b/supabase/migrations/20250305223244_add_pull_rpc_function.sql new file mode 100644 index 00000000..31ef819c --- /dev/null +++ b/supabase/migrations/20250305223244_add_pull_rpc_function.sql @@ -0,0 +1,25 @@ +create type "public"."pull_result" as ("client_groups" json, "facts" json); +CREATE OR REPLACE FUNCTION public.pull_data(token_id uuid, client_group_id text) + RETURNS pull_result + LANGUAGE plpgsql +AS $function$ +DECLARE + result pull_result; +BEGIN + -- Get client group data as JSON array + SELECT json_agg(row_to_json(rc)) + FROM replicache_clients rc + WHERE rc.client_group = client_group_id + INTO result.client_groups; + + -- Get facts as JSON array + SELECT json_agg(row_to_json(f)) + FROM permission_tokens pt, + get_facts(pt.root_entity) f + WHERE pt.id = token_id + INTO result.facts; + + RETURN result; +END; +$function$ +;