From cc166fd35a8c25ee4b5e944c48b51a52c2b826ff Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 7 Jul 2026 20:41:08 -0400 Subject: [PATCH] eng-1910-add-no-op-content-resolve-endpoint --- .../api/supabase/space/[id]/concept/route.ts | 57 +++++++++++++ .../api/supabase/space/[id]/content/route.ts | 57 +++++++++++++ apps/website/app/utils/supabase/account.ts | 17 ++++ packages/database/src/crossAppContracts.ts | 5 ++ .../database/src/lib/crossAppConverters.ts | 80 +++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 apps/website/app/api/supabase/space/[id]/concept/route.ts create mode 100644 apps/website/app/api/supabase/space/[id]/content/route.ts diff --git a/apps/website/app/api/supabase/space/[id]/concept/route.ts b/apps/website/app/api/supabase/space/[id]/concept/route.ts new file mode 100644 index 000000000..c5305c2b9 --- /dev/null +++ b/apps/website/app/api/supabase/space/[id]/concept/route.ts @@ -0,0 +1,57 @@ +import { NextResponse, NextRequest } from "next/server"; + +import { createClient } from "~/utils/supabase/server"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { asPostgrestFailure } from "@repo/database/lib/contextFunctions"; +import type { Json } from "@repo/database/dbTypes"; +import { CrossAppNode } from "@repo/database/crossAppContracts"; +import { crossAppNodeToDbConcept } from "@repo/database/lib/crossAppConverters"; +import { getAccountId } from "~/utils/supabase/account"; + +type ApiParams = Promise<{ id: string }>; +export type SegmentDataType = { params: ApiParams }; + +export const POST = async ( + request: NextRequest, + segmentData: SegmentDataType, +): Promise => { + const { id: spaceIdS } = await segmentData.params; + const spaceId = Number.parseInt(spaceIdS); + if (Number.isNaN(spaceId)) + return createApiResponse( + request, + asPostgrestFailure("Cannot parse space id", "invalid", 403), + ); + + const supabase = await createClient(); + + const userId = await getAccountId(supabase); + if (userId === undefined) + return createApiResponse( + request, + asPostgrestFailure("Please login", "invalid", 401), + ); + try { + const body = (await request.json()) as CrossAppNode[]; + // TODO: Zed validator + const concepts = body + .map((c) => crossAppNodeToDbConcept(c)) + .filter((c) => c !== undefined); + if (concepts.length === 0) throw new Error("Could not translate concepts"); + const result = await supabase.rpc("upsert_concepts", { + data: concepts as Json, + v_space_id: spaceId, + v_creator_id: userId, + content_as_document: true, + }); + return createApiResponse(request, result); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/space/[id]/concept"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/space/[id]/content/route.ts b/apps/website/app/api/supabase/space/[id]/content/route.ts new file mode 100644 index 000000000..b1cf0edd2 --- /dev/null +++ b/apps/website/app/api/supabase/space/[id]/content/route.ts @@ -0,0 +1,57 @@ +import { NextResponse, NextRequest } from "next/server"; + +import { createClient } from "~/utils/supabase/server"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { asPostgrestFailure } from "@repo/database/lib/contextFunctions"; +import type { Json } from "@repo/database/dbTypes"; +import { StandaloneCrossAppContent } from "@repo/database/crossAppContracts"; +import { crossAppStandaloneContentToDbContent } from "@repo/database/lib/crossAppConverters"; +import { getAccountId } from "~/utils/supabase/account"; + +type ApiParams = Promise<{ id: string }>; +export type SegmentDataType = { params: ApiParams }; + +export const POST = async ( + request: NextRequest, + segmentData: SegmentDataType, +): Promise => { + const { id: spaceIdS } = await segmentData.params; + const spaceId = Number.parseInt(spaceIdS); + if (Number.isNaN(spaceId)) + return createApiResponse( + request, + asPostgrestFailure("Cannot parse space id", "invalid", 403), + ); + + const supabase = await createClient(); + + const userId = await getAccountId(supabase); + if (userId === undefined) + return createApiResponse( + request, + asPostgrestFailure("Please login", "invalid", 401), + ); + try { + const body = (await request.json()) as StandaloneCrossAppContent[]; + // TODO: Zed validator + const content = body + .map((c) => crossAppStandaloneContentToDbContent(c, { dbId: spaceId })) + .filter((c) => c !== undefined); + if (content.length === 0) throw new Error("Could not translate content"); + const result = await supabase.rpc("upsert_content", { + data: content as Json, + v_space_id: spaceId, + v_creator_id: userId, + content_as_document: true, + }); + return createApiResponse(request, result); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/space/[id]/content"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/utils/supabase/account.ts b/apps/website/app/utils/supabase/account.ts index e7305f4c9..35c2d4efe 100644 --- a/apps/website/app/utils/supabase/account.ts +++ b/apps/website/app/utils/supabase/account.ts @@ -3,6 +3,23 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; type AgentType = Database["public"]["Enums"]["AgentType"] | "group"; +export const getAccountId = async ( + client: DGSupabaseClient, +): Promise => { + const { data, error } = await client.auth.getUser(); + if (error || !data?.user) return undefined; + const userData = data.user; + if (typeof userData.id !== "string") return undefined; + const id = userData.id; + const accountReq = await client + .from("PlatformAccount") + .select("id") + .eq("dg_account", id) + .eq("agent_type", "person") + .maybeSingle(); + return accountReq.data?.id; +}; + export const getSessionBaseUserData = async ( client: DGSupabaseClient, ): Promise<{ diff --git a/packages/database/src/crossAppContracts.ts b/packages/database/src/crossAppContracts.ts index c996332c3..d49ce24af 100644 --- a/packages/database/src/crossAppContracts.ts +++ b/packages/database/src/crossAppContracts.ts @@ -41,6 +41,11 @@ type InlineCrossAppTypedContent = InlineCrossAppContent & { contentType: ContentType; }; +export type StandaloneCrossAppContent = InlineCrossAppTypedContent & + CrossAppBase & { + variant: Enums<"ContentVariant">; + }; + // A node instance export type CrossAppNode = CrossAppBase & { nodeType: Ref; diff --git a/packages/database/src/lib/crossAppConverters.ts b/packages/database/src/lib/crossAppConverters.ts index d5c9dfc9f..0ae19da0f 100644 --- a/packages/database/src/lib/crossAppConverters.ts +++ b/packages/database/src/lib/crossAppConverters.ts @@ -3,12 +3,14 @@ import { Ref, CrossAppEmbedding, InlineCrossAppContent, + StandaloneCrossAppContent, CrossAppBase, CrossAppNode, } from "../crossAppContracts"; import { LocalContentDataInput, LocalConceptDataInput } from "../inputTypes"; import { Enums, CompositeTypes } from "../dbTypes"; +type ContentDataInput = CompositeTypes<"content_local_input">; type InlineEmbeddingInput = CompositeTypes<"inline_embedding_input">; type InlineAbstractBase = Partial; @@ -39,6 +41,47 @@ const decodeRef = ( return decodeLocalRef(ref, localVarName); }; +const decodeRefWithNulls = < + DbVarName extends string, + LocalVarName extends string, +>( + ref: Ref | undefined, + dbVarName: DbVarName, + localVarName: LocalVarName, +): Record & Record => { + return { + [dbVarName]: ref && "dbId" in ref ? ref.dbId : null, + [localVarName]: ref && "localId" in ref ? ref.localId : null, + } as Record & Record; +}; + +const decodeRefWithInlineNulls = < + DbVarName extends string, + LocalVarName extends string, + InlineVarName extends string, +>({ + ref, + dbVarName, + localVarName, + inlineVarName, +}: { + ref?: Ref; + dbVarName: DbVarName; + localVarName: LocalVarName; + inlineVarName: InlineVarName; +}): Record & + Record & + Record => { + return { + [dbVarName]: null, + [localVarName]: null, + [inlineVarName]: null, + ...decodeRef(ref, dbVarName, localVarName), + } as Record & + Record & + Record; +}; + const crossAppEmbeddingToDbEmbedding = ( embedding: CrossAppEmbedding | undefined, ): InlineEmbeddingInput | undefined => @@ -76,6 +119,43 @@ const inlineCrossAppContentToDbContent = ( }); }; +export const crossAppStandaloneContentToDbContent = ( + content: StandaloneCrossAppContent | undefined, + space: Ref, +): ContentDataInput | undefined => { + if (content === undefined) return undefined; + return { + source_local_id: content.localId, + text: content.value, + scale: content.scale || "document", + content_type: content.contentType || "text/plain", + variant: content.variant, + created: content.createdAt.toISOString(), + last_modified: (content.modifiedAt || content.createdAt).toISOString(), + embedding_inline: crossAppEmbeddingToDbEmbedding(content.embedding) || null, + ...decodeRefWithInlineNulls({ + ref: content.author, + dbVarName: "author_id", + localVarName: "author_local_id", + inlineVarName: "author_inline", + }), + ...decodeRefWithNulls(space, "space_id", "space_url"), + // provide other explicit null values for type completion + ...decodeRefWithInlineNulls({ + dbVarName: "creator_id", + localVarName: "creator_local_id", + inlineVarName: "creator_inline", + }), + ...decodeRefWithInlineNulls({ + dbVarName: "document_id", + localVarName: "document_local_id", + inlineVarName: "document_inline", + }), + ...decodeRefWithNulls(undefined, "part_of_id", "part_of_local_id"), + metadata: null, + }; +}; + export const crossAppNodeToDbContent = ( node: CrossAppNode | undefined, variant: "full" | "direct",