Skip to content

Commit 7fc6f80

Browse files
committed
eng-1909-add-no-op-content-upsert-endpoint
1 parent 83cd593 commit 7fc6f80

4 files changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
3+
import { createClient } from "~/utils/supabase/server";
4+
import {
5+
createApiResponse,
6+
handleRouteError,
7+
defaultOptionsHandler,
8+
} from "~/utils/supabase/apiUtils";
9+
import { asPostgrestFailure } from "@repo/database/lib/contextFunctions";
10+
import type { Json } from "@repo/database/dbTypes";
11+
import { StandaloneCrossAppContent } from "@repo/database/crossAppContracts";
12+
import { crossAppStandaloneContentToDbContent } from "@repo/database/lib/crossAppConverters";
13+
import { getAccountId } from "~/utils/supabase/account";
14+
15+
type ApiParams = Promise<{ id: string }>;
16+
export type SegmentDataType = { params: ApiParams };
17+
18+
export const POST = async (
19+
request: NextRequest,
20+
segmentData: SegmentDataType,
21+
): Promise<NextResponse> => {
22+
const { id: spaceIdS } = await segmentData.params;
23+
const spaceId = Number.parseInt(spaceIdS);
24+
if (Number.isNaN(spaceId))
25+
return createApiResponse(
26+
request,
27+
asPostgrestFailure("Cannot parse space id", "invalid", 403),
28+
);
29+
30+
const supabase = await createClient();
31+
32+
const userId = await getAccountId(supabase);
33+
if (userId === undefined)
34+
return createApiResponse(
35+
request,
36+
asPostgrestFailure("Please login", "invalid", 401),
37+
);
38+
try {
39+
const body = (await request.json()) as StandaloneCrossAppContent[];
40+
// TODO: Zed validator
41+
const content = body
42+
.map((c) => crossAppStandaloneContentToDbContent(c, { dbId: spaceId }))
43+
.filter((c) => c !== undefined);
44+
if (content.length === 0) throw new Error("Could not translate content");
45+
const result = await supabase.rpc("upsert_content", {
46+
data: content as Json,
47+
v_space_id: spaceId,
48+
v_creator_id: userId,
49+
content_as_document: true,
50+
});
51+
return createApiResponse(request, result);
52+
} catch (e: unknown) {
53+
return handleRouteError(request, e, "/api/supabase/space/[id]/content");
54+
}
55+
};
56+
57+
export const OPTIONS = defaultOptionsHandler;

apps/website/app/utils/supabase/account.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import type { DGSupabaseClient } from "@repo/database/lib/client";
33

44
type AgentType = Database["public"]["Enums"]["AgentType"] | "group";
55

6+
export const getAccountId = async (
7+
client: DGSupabaseClient,
8+
): Promise<number | undefined> => {
9+
const { data, error } = await client.auth.getUser();
10+
if (error || !data?.user) return undefined;
11+
const userData = data.user;
12+
if (typeof userData.id !== "string") return undefined;
13+
const id = userData.id;
14+
const accountReq = await client
15+
.from("PlatformAccount")
16+
.select("id")
17+
.eq("dg_account", id)
18+
.eq("agent_type", "person")
19+
.maybeSingle();
20+
return accountReq.data?.id;
21+
};
22+
623
export const getSessionBaseUserData = async (
724
client: DGSupabaseClient,
825
): Promise<{

packages/database/src/crossAppContracts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ type InlineCrossAppTypedContent = InlineCrossAppContent & {
9999
contentType: ContentType;
100100
};
101101

102+
export type StandaloneCrossAppContent = InlineCrossAppTypedContent &
103+
CrossAppBase & {
104+
variant: Enums<"ContentVariant">;
105+
};
106+
102107
// A node instance
103108
export type CrossAppNode = CrossAppBase & {
104109
nodeType: Ref;

packages/database/src/lib/crossAppConverters.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
LocalOrRemoteRef,
55
CrossAppEmbedding,
66
InlineCrossAppContent,
7+
StandaloneCrossAppContent,
78
CrossAppBase,
89
CrossAppNode,
910
CrossAppNodeSchema,
@@ -15,6 +16,7 @@ import { LocalContentDataInput, LocalConceptDataInput } from "../inputTypes";
1516
import { Enums, CompositeTypes } from "../dbTypes";
1617
import { spaceUriAndLocalIdToRid, ridToSpaceUriAndLocalId } from "./rid";
1718

19+
type ContentDataInput = CompositeTypes<"content_local_input">;
1820
type InlineEmbeddingInput = CompositeTypes<"inline_embedding_input">;
1921
type InlineAbstractBase = Partial<CrossAppBase>;
2022

@@ -45,6 +47,47 @@ const decodeRef = <DbVarName extends string, LocalVarName extends string>(
4547
return decodeLocalRef(ref, localVarName);
4648
};
4749

50+
const decodeRefWithNulls = <
51+
DbVarName extends string,
52+
LocalVarName extends string,
53+
>(
54+
ref: Ref | undefined,
55+
dbVarName: DbVarName,
56+
localVarName: LocalVarName,
57+
): Record<DbVarName, number | null> & Record<LocalVarName, string | null> => {
58+
return {
59+
[dbVarName]: ref && "dbId" in ref ? ref.dbId : null,
60+
[localVarName]: ref && "localId" in ref ? ref.localId : null,
61+
} as Record<DbVarName, number | null> & Record<LocalVarName, string | null>;
62+
};
63+
64+
const decodeRefWithInlineNulls = <
65+
DbVarName extends string,
66+
LocalVarName extends string,
67+
InlineVarName extends string,
68+
>({
69+
ref,
70+
dbVarName,
71+
localVarName,
72+
inlineVarName,
73+
}: {
74+
ref?: Ref;
75+
dbVarName: DbVarName;
76+
localVarName: LocalVarName;
77+
inlineVarName: InlineVarName;
78+
}): Record<DbVarName, number | null> &
79+
Record<LocalVarName, string | null> &
80+
Record<InlineVarName, null> => {
81+
return {
82+
[dbVarName]: null,
83+
[localVarName]: null,
84+
[inlineVarName]: null,
85+
...decodeRef(ref, dbVarName, localVarName),
86+
} as Record<DbVarName, number | null> &
87+
Record<LocalVarName, string | null> &
88+
Record<InlineVarName, null>;
89+
};
90+
4891
const decodeLocalOrRemoteRef = <
4992
LocalVarName extends string,
5093
DbVarName extends string,
@@ -138,6 +181,43 @@ const inlineCrossAppContentToDbContent = (
138181
});
139182
};
140183

184+
export const crossAppStandaloneContentToDbContent = (
185+
content: StandaloneCrossAppContent | undefined,
186+
space: Ref,
187+
): ContentDataInput | undefined => {
188+
if (content === undefined) return undefined;
189+
return {
190+
source_local_id: content.localId,
191+
text: content.value,
192+
scale: content.scale || "document",
193+
content_type: content.contentType || "text/plain",
194+
variant: content.variant,
195+
created: content.createdAt.toISOString(),
196+
last_modified: (content.modifiedAt || content.createdAt).toISOString(),
197+
embedding_inline: crossAppEmbeddingToDbEmbedding(content.embedding) || null,
198+
...decodeRefWithInlineNulls({
199+
ref: content.author,
200+
dbVarName: "author_id",
201+
localVarName: "author_local_id",
202+
inlineVarName: "author_inline",
203+
}),
204+
...decodeRefWithNulls(space, "space_id", "space_url"),
205+
// provide other explicit null values for type completion
206+
...decodeRefWithInlineNulls({
207+
dbVarName: "creator_id",
208+
localVarName: "creator_local_id",
209+
inlineVarName: "creator_inline",
210+
}),
211+
...decodeRefWithInlineNulls({
212+
dbVarName: "document_id",
213+
localVarName: "document_local_id",
214+
inlineVarName: "document_inline",
215+
}),
216+
...decodeRefWithNulls(undefined, "part_of_id", "part_of_local_id"),
217+
metadata: null,
218+
};
219+
};
220+
141221
export const crossAppNodeToDbContent = (
142222
node: CrossAppNode | undefined,
143223
variant: "full" | "direct",

0 commit comments

Comments
 (0)