Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,6 @@ export const roamClaimFullMarkdownExample: {
full: {
contentType: contentTypes.markdown,
value: buildFullMarkdown({ title, blocks }),
author: { localId: "someone" },
authorId: "someone",
},
};
4 changes: 2 additions & 2 deletions apps/roam/src/utils/convertRoamNodeToFullContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export const convertRoamNodeToFullContent = ({
const blocks = getFullTreeByParentUid(node.source_local_id).children;
const viewType = getPageViewType(title) || "bullet";
const crossAppNode: CrossAppNode = {
author: { localId: node.author_local_id },
authorId: node.author_local_id,
localId: node.source_local_id,
createdAt: new Date(node.created || Date.now()),
modifiedAt: new Date(node.last_modified || Date.now()),
nodeType: { localId: node.node_type_id },
nodeType: node.node_type_id,
content: {
direct: {
localId: node.source_local_id,
Expand Down
66 changes: 66 additions & 0 deletions apps/website/app/api/internal/space/[id]/content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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();

try {
const userId = await getAccountId(supabase);
if (userId === undefined)
return createApiResponse(
request,
asPostgrestFailure("Please login", "invalid", 401),
);
const body = (await request.json()) as StandaloneCrossAppContent[];
// TODO: Zed validator
const content = body
.map((c) =>
crossAppStandaloneContentToDbContent(
{
...c,
createdAt: new Date(c.createdAt),
modifiedAt: new Date(c.modifiedAt || c.createdAt),
},
spaceId,
),
)
.filter((c) => c !== undefined);
Comment thread
maparent marked this conversation as resolved.
Comment thread
maparent marked this conversation as resolved.
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;
18 changes: 18 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,24 @@ 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();
if (accountReq.error) throw accountReq.error;
return accountReq.data?.id;
Comment thread
maparent marked this conversation as resolved.
};

export const getSessionBaseUserData = async (
client: DGSupabaseClient,
): Promise<{
Expand Down
32 changes: 14 additions & 18 deletions packages/database/src/crossAppContracts.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import type { ContentType } from "@repo/content-model";
import { Enums, type Json } from "./dbTypes";

export type LocalRef = {
// This localId is expected to be unique within the current space
localId: string;
};

type DbRef = {
// Some operations will refer to objects through their database Id
dbId: number;
};

// Generalized reference
export type Ref = LocalRef | DbRef;
// An identifier for objects in the platform. Expected to be unique within the platform.
export type LocalId = string;

// Common attributes for most types
export type CrossAppBase = LocalRef & {
export type CrossAppBase = {
localId: LocalId;
createdAt: Date;
modifiedAt?: Date;
author: Ref;
authorId: LocalId;
};

export type CrossAppSchemaBase = CrossAppBase & {
Expand Down Expand Up @@ -48,13 +39,13 @@ export type CrossAppRelationTripleSchema = CrossAppSchemaBase &
relation?: never;
}
| {
relation: Ref;
relation: LocalId;
label?: never;
complement?: never;
}
) & {
sourceType: Ref;
destinationType: Ref;
sourceType: LocalId;
destinationType: LocalId;
};

// An inline vector semantic embedding
Expand All @@ -77,9 +68,14 @@ type InlineCrossAppTypedContent = InlineCrossAppContent & {
contentType: ContentType;
};

export type StandaloneCrossAppContent = InlineCrossAppTypedContent &
CrossAppBase & {
variant: Enums<"ContentVariant">;
};

// A node instance
export type CrossAppNode = CrossAppBase & {
nodeType: Ref;
nodeType: LocalId;
content: {
direct: InlineCrossAppContent;
full?: InlineCrossAppTypedContent;
Expand Down
20 changes: 8 additions & 12 deletions packages/database/src/crossAppNodeContract.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,21 @@ Multiple studies show that sleep after learning strengthens memory traces.

export const roamOriginNodeExample: CrossAppNode = {
localId: ROAM_SOURCE_NODE_ID,
nodeType: {
localId: "rCLM0schema",
},
nodeType: "rCLM0schema",
content: {
direct: {
value: "Sleep improves memory consolidation",
author: { localId: "someone" },
authorId: "someone",
},
full: {
contentType: contentTypes.markdown,
value: roamFullMarkdown,
author: { localId: "someone" },
authorId: "someone",
},
},
createdAt: new Date("2026-06-12T14:00:00.000Z"),
modifiedAt: new Date("2026-06-12T15:00:00.000Z"),
author: { localId: "maparent" },
authorId: "maparent",
};

// const OBSIDIAN_SOURCE_SPACE_ID = "obsidian:9a8b7c6d5e4f3210";
Expand All @@ -48,21 +46,19 @@ Participants with more REM sleep showed better next-day recall.

export const obsidianOriginNodeExample: CrossAppNode = {
localId: OBSIDIAN_SOURCE_NODE_ID,
nodeType: {
localId: OBSIDIAN_SOURCE_NODE_TYPE_ID,
},
nodeType: OBSIDIAN_SOURCE_NODE_TYPE_ID,
content: {
direct: {
value: "EVD - REM sleep and recall",
author: { localId: "someone" },
authorId: "someone",
},
full: {
contentType: contentTypes.markdown,
value: obsidianFullMarkdown,
author: { localId: "someone" },
authorId: "someone",
},
},
createdAt: new Date("2026-06-14T10:30:00.000Z"),
modifiedAt: new Date("2026-06-14T15:00:00.000Z"),
author: { localId: "maparent" },
authorId: "maparent",
};
77 changes: 40 additions & 37 deletions packages/database/src/lib/crossAppConverters.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,14 @@
import {
LocalRef,
Ref,
CrossAppEmbedding,
InlineCrossAppContent,
CrossAppBase,
StandaloneCrossAppContent,
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>;

const decodeLocalRef = <LocalVarName extends string>(
ref: LocalRef | InlineAbstractBase | undefined,
localVarName: LocalVarName,
): Record<LocalVarName, string> | Record<string, never> => {
if (ref === undefined) return {};
if ("localId" in ref) {
return {
[localVarName]: ref.localId,
} as Record<LocalVarName, string>;
}
return {};
};

const decodeRef = <DbVarName extends string, LocalVarName extends string>(
ref: Ref | undefined,
dbVarName: DbVarName,
localVarName: LocalVarName,
):
| Record<DbVarName, number>
| Record<LocalVarName, string>
| Record<string, never> => {
if (ref === undefined) return {};
if ("dbId" in ref)
return { [dbVarName]: ref.dbId } as Record<DbVarName, number>;
return decodeLocalRef(ref, localVarName);
};

const crossAppEmbeddingToDbEmbedding = (
embedding: CrossAppEmbedding | undefined,
Expand All @@ -64,18 +35,50 @@ const inlineCrossAppContentToDbContent = (
): LocalContentDataInput | undefined => {
if (content === undefined) return undefined;
return filterUndefined<LocalContentDataInput>({
...decodeLocalRef(content, "source_local_id"),
source_local_id: content.localId,
text: content.value,
scale: content.scale || "document",
content_type: content.contentType || "text/plain",
variant,
created: content.createdAt?.toISOString(),
last_modified: content.modifiedAt?.toISOString(),
...decodeRef(content.author, "author_id", "author_local_id"),
author_local_id: content.authorId,
embedding_inline: crossAppEmbeddingToDbEmbedding(content.embedding),
});
};

export const crossAppStandaloneContentToDbContent = (
content: StandaloneCrossAppContent | undefined,
spaceId: number,
): ContentDataInput | undefined => {
if (content === undefined) return undefined;
return {
source_local_id: content.localId,
author_local_id: content.authorId,
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(),
Comment thread
maparent marked this conversation as resolved.
embedding_inline: crossAppEmbeddingToDbEmbedding(content.embedding) || null,
space_id: spaceId,
// provide other explicit null values for type completion
space_url: null,
author_inline: null,
author_id: null,
document_id: null,
creator_id: null,
creator_local_id: null,
creator_inline: null,
document_local_id: null,
document_inline: null,
part_of_id: null,
part_of_local_id: null,
metadata: null,
};
};

export const crossAppNodeToDbContent = (
node: CrossAppNode | undefined,
variant: "full" | "direct",
Expand All @@ -88,7 +91,7 @@ export const crossAppNodeToDbContent = (
...content,
createdAt: content.createdAt || node.createdAt,
modifiedAt: content.modifiedAt || node.modifiedAt,
author: content.author || node.author,
authorId: content.authorId || node.authorId,
},
variant,
);
Expand All @@ -98,10 +101,10 @@ export const crossAppNodeToDbConcept = (
node: CrossAppNode,
): LocalConceptDataInput => {
return filterUndefined<LocalConceptDataInput>({
...decodeLocalRef(node, "source_local_id"),
source_local_id: node.localId,
name: node.content.direct.value,
...decodeRef(node.author, "author_id", "author_local_id"),
...decodeRef(node.nodeType, "schema_id", "schema_represented_by_local_id"),
author_local_id: node.authorId,
schema_represented_by_local_id: node.nodeType,
contents_inline: filterUndefinedArray([
crossAppNodeToDbContent(node, "direct"),
crossAppNodeToDbContent(node, "full"),
Expand Down