Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apps/website/app/api/supabase/space/[id]/concept/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse> => {
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;
57 changes: 57 additions & 0 deletions apps/website/app/api/supabase/space/[id]/content/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse> => {
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;
17 changes: 17 additions & 0 deletions apps/website/app/utils/supabase/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | undefined> => {
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<{
Expand Down
5 changes: 5 additions & 0 deletions packages/database/src/crossAppContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions packages/database/src/lib/crossAppConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CrossAppBase>;

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

const decodeRefWithNulls = <
DbVarName extends string,
LocalVarName extends string,
>(
ref: Ref | undefined,
dbVarName: DbVarName,
localVarName: LocalVarName,
): Record<DbVarName, number | null> & Record<LocalVarName, string | null> => {
return {
[dbVarName]: ref && "dbId" in ref ? ref.dbId : null,
[localVarName]: ref && "localId" in ref ? ref.localId : null,
} as Record<DbVarName, number | null> & Record<LocalVarName, string | null>;
};

const decodeRefWithInlineNulls = <
DbVarName extends string,
LocalVarName extends string,
InlineVarName extends string,
>({
ref,
dbVarName,
localVarName,
inlineVarName,
}: {
ref?: Ref;
dbVarName: DbVarName;
localVarName: LocalVarName;
inlineVarName: InlineVarName;
}): Record<DbVarName, number | null> &
Record<LocalVarName, string | null> &
Record<InlineVarName, null> => {
return {
[dbVarName]: null,
[localVarName]: null,
[inlineVarName]: null,
...decodeRef(ref, dbVarName, localVarName),
} as Record<DbVarName, number | null> &
Record<LocalVarName, string | null> &
Record<InlineVarName, null>;
};

const crossAppEmbeddingToDbEmbedding = (
embedding: CrossAppEmbedding | undefined,
): InlineEmbeddingInput | undefined =>
Expand Down Expand Up @@ -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",
Expand Down