Skip to content

Commit d43d61e

Browse files
committed
eng-1909-add-no-op-content-upsert-endpoint
1 parent d54f0c7 commit d43d61e

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
@@ -77,6 +77,11 @@ type InlineCrossAppTypedContent = InlineCrossAppContent & {
7777
contentType: ContentType;
7878
};
7979

80+
export type StandaloneCrossAppContent = InlineCrossAppTypedContent &
81+
CrossAppBase & {
82+
variant: Enums<"ContentVariant">;
83+
};
84+
8085
// A node instance
8186
export type CrossAppNode = CrossAppBase & {
8287
nodeType: Ref;

packages/database/src/lib/crossAppConverters.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import {
33
Ref,
44
CrossAppEmbedding,
55
InlineCrossAppContent,
6+
StandaloneCrossAppContent,
67
CrossAppBase,
78
CrossAppNode,
89
} from "../crossAppContracts";
910
import { LocalContentDataInput, LocalConceptDataInput } from "../inputTypes";
1011
import { Enums, CompositeTypes } from "../dbTypes";
1112

13+
type ContentDataInput = CompositeTypes<"content_local_input">;
1214
type InlineEmbeddingInput = CompositeTypes<"inline_embedding_input">;
1315
type InlineAbstractBase = Partial<CrossAppBase>;
1416

@@ -39,6 +41,47 @@ const decodeRef = <DbVarName extends string, LocalVarName extends string>(
3941
return decodeLocalRef(ref, localVarName);
4042
};
4143

44+
const decodeRefWithNulls = <
45+
DbVarName extends string,
46+
LocalVarName extends string,
47+
>(
48+
ref: Ref | undefined,
49+
dbVarName: DbVarName,
50+
localVarName: LocalVarName,
51+
): Record<DbVarName, number | null> & Record<LocalVarName, string | null> => {
52+
return {
53+
[dbVarName]: ref && "dbId" in ref ? ref.dbId : null,
54+
[localVarName]: ref && "localId" in ref ? ref.localId : null,
55+
} as Record<DbVarName, number | null> & Record<LocalVarName, string | null>;
56+
};
57+
58+
const decodeRefWithInlineNulls = <
59+
DbVarName extends string,
60+
LocalVarName extends string,
61+
InlineVarName extends string,
62+
>({
63+
ref,
64+
dbVarName,
65+
localVarName,
66+
inlineVarName,
67+
}: {
68+
ref?: Ref;
69+
dbVarName: DbVarName;
70+
localVarName: LocalVarName;
71+
inlineVarName: InlineVarName;
72+
}): Record<DbVarName, number | null> &
73+
Record<LocalVarName, string | null> &
74+
Record<InlineVarName, null> => {
75+
return {
76+
[dbVarName]: null,
77+
[localVarName]: null,
78+
[inlineVarName]: null,
79+
...decodeRef(ref, dbVarName, localVarName),
80+
} as Record<DbVarName, number | null> &
81+
Record<LocalVarName, string | null> &
82+
Record<InlineVarName, null>;
83+
};
84+
4285
const crossAppEmbeddingToDbEmbedding = (
4386
embedding: CrossAppEmbedding | undefined,
4487
): InlineEmbeddingInput | undefined =>
@@ -76,6 +119,43 @@ const inlineCrossAppContentToDbContent = (
76119
});
77120
};
78121

122+
export const crossAppStandaloneContentToDbContent = (
123+
content: StandaloneCrossAppContent | undefined,
124+
space: Ref,
125+
): ContentDataInput | undefined => {
126+
if (content === undefined) return undefined;
127+
return {
128+
source_local_id: content.localId,
129+
text: content.value,
130+
scale: content.scale || "document",
131+
content_type: content.contentType || "text/plain",
132+
variant: content.variant,
133+
created: content.createdAt.toISOString(),
134+
last_modified: (content.modifiedAt || content.createdAt).toISOString(),
135+
embedding_inline: crossAppEmbeddingToDbEmbedding(content.embedding) || null,
136+
...decodeRefWithInlineNulls({
137+
ref: content.author,
138+
dbVarName: "author_id",
139+
localVarName: "author_local_id",
140+
inlineVarName: "author_inline",
141+
}),
142+
...decodeRefWithNulls(space, "space_id", "space_url"),
143+
// provide other explicit null values for type completion
144+
...decodeRefWithInlineNulls({
145+
dbVarName: "creator_id",
146+
localVarName: "creator_local_id",
147+
inlineVarName: "creator_inline",
148+
}),
149+
...decodeRefWithInlineNulls({
150+
dbVarName: "document_id",
151+
localVarName: "document_local_id",
152+
inlineVarName: "document_inline",
153+
}),
154+
...decodeRefWithNulls(undefined, "part_of_id", "part_of_local_id"),
155+
metadata: null,
156+
};
157+
};
158+
79159
export const crossAppNodeToDbContent = (
80160
node: CrossAppNode | undefined,
81161
variant: "full" | "direct",

0 commit comments

Comments
 (0)