diff --git a/apps/roam/src/utils/publishNodesToGroups.ts b/apps/roam/src/utils/publishNodesToGroups.ts index 180439a51..a3a280033 100644 --- a/apps/roam/src/utils/publishNodesToGroups.ts +++ b/apps/roam/src/utils/publishNodesToGroups.ts @@ -2,8 +2,11 @@ import { CrossAppNode } from "@repo/database/crossAppContracts"; import type { DGSupabaseClient } from "@repo/database/lib/client"; import { getAvailableGroupIds } from "@repo/database/lib/groups"; import { nodeUidsWithTypeToCrossApp } from "./roamToCrossAppConverters"; +import { nodeSchemaToCrossApp } from "./roamToCrossAppConverters"; +import { crossAppNodeSchemaToDbConcept } from "@repo/database/lib/crossAppConverters"; import { ensurePartialSpaceAccess } from "@repo/database/lib/groups"; import { isIgnorableUpsertError } from "@repo/database/lib/contextFunctions"; +import getDiscourseNodes from "./getDiscourseNodes"; import { difference, intersection } from "@repo/utils/setOperations"; import internalError from "./internalError"; @@ -16,7 +19,10 @@ const onlyStrings = (values: (string | null)[]): string[] => values.filter((value): value is string => typeof value === "string"); type PublishNodesResult = { + publishedNodeSchemaUids: string[]; publishedNodeUids: string[]; + syncedNodeSchemaUids: string[]; + failedSyncedUids: string[]; skippedUnsyncedUids: string[]; okGroupIds: string[]; failedGroupIds: string[]; @@ -42,7 +48,10 @@ export const publishNodesToGroups = async ({ nodes: CrossAppNode[]; }): Promise => { const result: PublishNodesResult = { + publishedNodeSchemaUids: [], publishedNodeUids: [], + syncedNodeSchemaUids: [], + failedSyncedUids: [], skippedUnsyncedUids: [], okGroupIds: [], failedGroupIds: [], @@ -71,12 +80,19 @@ export const publishNodesToGroups = async ({ if (groupIds.length === 0) return result; let nodeUids = [...new Set(nodes.map((node) => node.localId))]; + const nodeSchemaUids = new Set(nodes.map((node) => node.nodeType)); + const nodeSchemas = getDiscourseNodes() + .filter((s) => nodeSchemaUids.has(s.type)) + .map((s) => nodeSchemaToCrossApp(s)) + .filter((s) => s !== null); + + const neededUids = [...nodeSchemaUids, ...nodeUids]; const syncedRes = await client .from("my_concepts") .select("source_local_id") .eq("space_id", spaceId) - .in("source_local_id", nodeUids); + .in("source_local_id", neededUids); if (syncedRes.error) { internalError({ error: syncedRes.error }); return result; @@ -86,11 +102,18 @@ export const publishNodesToGroups = async ({ ); result.skippedUnsyncedUids = nodeUids.filter((uid) => !syncedUids.has(uid)); nodeUids = [...intersection(syncedUids, new Set(nodeUids))]; + const missingNodeSchemas = nodeSchemas.filter( + (s) => !syncedUids.has(s.localId), + ); + const upsertConcepts = [ + ...missingNodeSchemas.map((s) => crossAppNodeSchemaToDbConcept(s)), + ].filter((r) => r !== undefined); const resourceAccesses = []; + const resourceIds = [...nodeUids, ...nodeSchemaUids]; for (const groupId of groupIds) { resourceAccesses.push( - ...nodeUids.map((sourceLocalId) => ({ + ...resourceIds.map((sourceLocalId) => ({ account_uid: groupId, source_local_id: sourceLocalId, space_id: spaceId, @@ -98,6 +121,28 @@ export const publishNodesToGroups = async ({ ); } + if (upsertConcepts.length > 0) { + const response = await client.rpc("upsert_concepts", { + v_space_id: spaceId, + data: upsertConcepts, + }); + if (response.error) { + internalError({ error: response.error }); + return result; + } + const syncedSchemaUids = new Set(missingNodeSchemas.map((s) => s.localId)); + response.data.forEach((v, i) => { + if (v === -1) { + const localId = upsertConcepts[i].source_local_id; + if (localId) { + if (syncedSchemaUids.has(localId)) syncedSchemaUids.delete(localId); + result.failedSyncedUids.push(localId); + } + } + }); + result.syncedNodeSchemaUids = [...syncedSchemaUids]; + } + const grantRes = await client .from("ResourceAccess") .upsert(resourceAccesses, { ignoreDuplicates: true }); @@ -108,6 +153,7 @@ export const publishNodesToGroups = async ({ } result.okGroupIds = groupIds; + result.publishedNodeSchemaUids = [...nodeSchemaUids]; result.publishedNodeUids = nodeUids; return result; diff --git a/apps/roam/src/utils/roamToCrossAppConverters.ts b/apps/roam/src/utils/roamToCrossAppConverters.ts index bc7b9b77d..d432ac1e7 100644 --- a/apps/roam/src/utils/roamToCrossAppConverters.ts +++ b/apps/roam/src/utils/roamToCrossAppConverters.ts @@ -1,4 +1,7 @@ -import type { CrossAppNode } from "@repo/database/crossAppContracts"; +import type { + CrossAppNode, + CrossAppNodeSchema, +} from "@repo/database/crossAppContracts"; import type { RoamFullContentNode } from "./convertRoamNodeToFullContent"; import type { DiscourseNode } from "./getDiscourseNodes"; import type { TreeNode, ViewType } from "roamjs-components/types"; @@ -116,3 +119,25 @@ export const nodeUidsWithTypeToCrossApp = async ( }); return results; }; + +export const nodeSchemaToCrossApp = ( + s: DiscourseNode, +): CrossAppNodeSchema | null => { + const relData = window.roamAlphaAPI.pull( + "[:create/time :edit/time {:create/user [:user/uid]}]", + `[:block/uid "${s.type}"]`, + ) as unknown as { + ":create/time": number; + ":edit/time": number; + ":create/user": { ":user/uid": string }; + }; + if (!relData) return null; + const userUid = (relData[":create/user"] ?? {})[":user/uid"]; + if (!userUid) return null; + return { + localId: s.type, + label: s.text, + authorId: userUid, + createdAt: new Date(relData[":create/time"] || Date.now()), + }; +}; diff --git a/packages/database/src/inputTypes.ts b/packages/database/src/inputTypes.ts index 5ff4b76a8..64f69c635 100644 --- a/packages/database/src/inputTypes.ts +++ b/packages/database/src/inputTypes.ts @@ -6,7 +6,7 @@ export type LocalAccountDataInput = Partial< export type LocalDocumentDataInput = Partial< Omit< Database["public"]["CompositeTypes"]["document_local_input"], - "author_inline" + "author_inline" | "contents" > & { author_inline: LocalAccountDataInput } >; export type LocalContentDataInput = Partial<