From de9dff5b8d2b8e410bb422bbed01d94c16659086 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 15 Jan 2026 12:49:40 -0500 Subject: [PATCH] eng-1475 sync relations including imported node --- apps/obsidian/src/utils/conceptConversion.ts | 15 +-- apps/obsidian/src/utils/publishNode.ts | 2 +- .../src/utils/syncDgNodesToSupabase.ts | 4 +- packages/database/src/dbTypes.ts | 15 +++ .../20260221193625_rid_functions.sql | 103 ++++++++++++++++++ .../database/supabase/schemas/concept.sql | 66 ++++++++--- 6 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 packages/database/supabase/migrations/20260221193625_rid_functions.sql diff --git a/apps/obsidian/src/utils/conceptConversion.ts b/apps/obsidian/src/utils/conceptConversion.ts index 53b740533..2136ae97f 100644 --- a/apps/obsidian/src/utils/conceptConversion.ts +++ b/apps/obsidian/src/utils/conceptConversion.ts @@ -231,13 +231,6 @@ export const relationInstanceToLocalConcept = ({ return null; } - if ( - sourceNode.frontmatter.importedFromRid || - destinationNode.frontmatter.importedFromRid - ) - return null; // punt relation to imported nodes for now. - // otherwise put the importedFromRid in source, dest. - /* eslint-disable @typescript-eslint/naming-convention */ const literal_content: Record = {}; if (importedFromRid) literal_content.importedFromRid = importedFromRid; @@ -252,8 +245,12 @@ export const relationInstanceToLocalConcept = ({ last_modified: new Date(lastModified ?? created).toISOString(), literal_content, local_reference_content: { - source, - destination, + source: + (sourceNode.frontmatter.importedFromRid as string | undefined) ?? + source, + destination: + (destinationNode.frontmatter.importedFromRid as string | undefined) ?? + destination, }, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/apps/obsidian/src/utils/publishNode.ts b/apps/obsidian/src/utils/publishNode.ts index fe6c538a0..3230aa3cd 100644 --- a/apps/obsidian/src/utils/publishNode.ts +++ b/apps/obsidian/src/utils/publishNode.ts @@ -182,7 +182,6 @@ export const publishNodeRelations = async ({ (fm.publishedToGroups as string[]) || []; if (!publishedToGroups.includes(myGroup)) return; } - if (fm.importedFromRid) return; // temporary, should be removed after eng-1475 relevantNodeTypeById[id] = fm.nodeTypeId as string; }); relations.map((relation) => { @@ -199,6 +198,7 @@ export const publishNodeRelations = async ({ resourceIds.add(triple.id); } }); + if (!resourceIds) return; const publishResponse = await client.from("ResourceAccess").upsert( [...resourceIds.values()].map((sourceLocalId: string) => ({ /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts index 589550659..5c3daa565 100644 --- a/apps/obsidian/src/utils/syncDgNodesToSupabase.ts +++ b/apps/obsidian/src/utils/syncDgNodesToSupabase.ts @@ -423,7 +423,7 @@ export const syncAllNodesAndRelations = async ( } console.debug("Supabase client:", supabaseClient); - const allNodes = await collectDiscourseNodesFromVault(plugin); + const allNodes = await collectDiscourseNodesFromVault(plugin, true); const changedNodeInstances = relationsOnly ? [] @@ -495,7 +495,7 @@ const convertDgToSupabaseConcepts = async ({ const nodeTypes = plugin.settings.nodeTypes ?? []; const relationTypes = plugin.settings.relationTypes ?? []; const discourseRelations = plugin.settings.discourseRelations ?? []; - allNodes = allNodes ?? (await collectDiscourseNodesFromVault(plugin)); + allNodes = allNodes ?? (await collectDiscourseNodesFromVault(plugin, true)); const allNodesById = Object.fromEntries( allNodes.map((n) => [n.nodeInstanceId, n]), ); diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index 733985c3d..1f3529b30 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -1586,6 +1586,20 @@ export type Database = { } Returns: string } + rid_or_local_id_to_concept_db_id: { + Args: { default_space_id: number; rid: string } + Returns: number + } + rid_to_space_id_and_local_id: { + Args: { rid: string } + Returns: Database["public"]["CompositeTypes"]["accessible_resource"] + SetofOptions: { + from: "*" + to: "accessible_resource" + isOneToOne: true + isSetofReturn: false + } + } schema_of_concept: | { Args: { concept: Database["public"]["Tables"]["Concept"]["Row"] } @@ -1967,3 +1981,4 @@ export const Constants = { }, }, } as const + diff --git a/packages/database/supabase/migrations/20260221193625_rid_functions.sql b/packages/database/supabase/migrations/20260221193625_rid_functions.sql new file mode 100644 index 000000000..7ccb1b822 --- /dev/null +++ b/packages/database/supabase/migrations/20260221193625_rid_functions.sql @@ -0,0 +1,103 @@ +CREATE OR REPLACE FUNCTION public.rid_to_space_id_and_local_id(rid VARCHAR) +RETURNS public.accessible_resource STRICT STABLE +SET search_path = '' +LANGUAGE plpgsql AS $$ +DECLARE + uri VARCHAR; + source_local_id VARCHAR; + source_id BIGINT; +BEGIN +source_local_id := split_part(rid, '/', -1); +IF length(source_local_id) = length(rid) THEN + RETURN (null, 'Not a Rid')::public.accessible_resource; +END IF; +uri := substr(rid, 1, length(rid) - length(source_local_id) - 1); +IF rid ~ '^orn:\w+\.\w+:.*$' THEN + uri := concat(split_part(split_part(uri, ':', 2), '.', 1), ':', split_part(uri, ':', 3)); +ELSE + IF rid ~ '^orn:\w+:.*$' THEN + uri := substr(uri, 5); + END IF; +END IF; +SELECT id INTO source_id FROM public."Space" where url=uri; +IF source_id IS NULL THEN + RETURN (null, concat('Cannot find ', uri))::public.accessible_resource; +END IF; +RETURN (source_id, source_local_id); +END; +$$; + +CREATE OR REPLACE FUNCTION public.rid_or_local_id_to_concept_db_id(rid VARCHAR, default_space_id BIGINT) +RETURNS BIGINT STRICT STABLE +SET search_path = '' +LANGUAGE plpgsql AS $$ +DECLARE r public.accessible_resource; +BEGIN + r := (SELECT public.rid_to_space_id_and_local_id(rid)); + IF r.space_id IS NULL THEN + RETURN (SELECT id FROM public."Concept" WHERE space_id = default_space_id AND source_local_id = rid); + ELSE + RETURN (SELECT id FROM public."Concept" WHERE space_id = r.space_id AND source_local_id = r.source_local_id); + END IF; +END; +$$; + +CREATE OR REPLACE FUNCTION public._local_concept_to_db_concept(data public.concept_local_input) +RETURNS public."Concept" STABLE +SET search_path = '' +LANGUAGE plpgsql +AS $$ +DECLARE + concept public."Concept"%ROWTYPE; + reference_content JSONB := jsonb_build_object(); + key varchar; + value JSONB; + ref_single_val BIGINT; + ref_array_val BIGINT[]; +BEGIN + -- not fan of going through json, but not finding how to populate a record by a different shape record + concept := jsonb_populate_record(NULL::public."Concept", to_jsonb(data)); + IF data.author_local_id IS NOT NULL THEN + SELECT id FROM public."PlatformAccount" + WHERE account_local_id = data.author_local_id INTO concept.author_id; + END IF; + IF data.represented_by_id IS NOT NULL THEN + SELECT space_id, source_local_id FROM public."Content" + WHERE id = data.represented_by_id INTO concept.space_id, concept.source_local_id; + END IF; + IF data.space_url IS NOT NULL THEN + SELECT id FROM public."Space" + WHERE url = data.space_url INTO concept.space_id; + END IF; + IF concept.source_local_id = '' THEN + concept.source_local_id := NULL; + END IF; + IF data.represented_by_local_id = '' THEN + data.represented_by_local_id := NULL; + END IF; + IF data.schema_represented_by_local_id IS NOT NULL THEN + SELECT public.rid_or_local_id_to_concept_db_id( + data.schema_represented_by_local_id, concept.space_id) INTO concept.schema_id; + END IF; + concept.source_local_id = COALESCE(concept.source_local_id, data.represented_by_local_id); -- legacy input field + concept.reference_content := coalesce(data.reference_content, '{}'::jsonb); + IF data.local_reference_content IS NOT NULL THEN + FOR key, value IN SELECT * FROM jsonb_each(data.local_reference_content) LOOP + IF jsonb_typeof(value) = 'array' THEN + WITH el AS (SELECT jsonb_array_elements_text(value) as x), + el2 AS (SELECT public.rid_or_local_id_to_concept_db_id(x, concept.space_id) AS id FROM el) + SELECT array_agg(DISTINCT el2.id) INTO STRICT ref_array_val + FROM el2 WHERE el2.id IS NOT NULL; + reference_content := jsonb_set(reference_content, ARRAY[key], to_jsonb(ref_array_val)); + ELSIF jsonb_typeof(value) = 'string' THEN + SELECT public.rid_or_local_id_to_concept_db_id(value #>> '{}', concept.space_id) INTO STRICT ref_single_val; + reference_content := jsonb_set(reference_content, ARRAY[key], to_jsonb(ref_single_val)); + ELSE + RAISE EXCEPTION 'Invalid value in local_reference_content % %', value, jsonb_typeof(value); + END IF; + END LOOP; + concept.reference_content := concept.reference_content || reference_content; + END IF; + RETURN concept; +END; +$$; diff --git a/packages/database/supabase/schemas/concept.sql b/packages/database/supabase/schemas/concept.sql index 2b1a6ba8a..909cf44dc 100644 --- a/packages/database/supabase/schemas/concept.sql +++ b/packages/database/supabase/schemas/concept.sql @@ -240,6 +240,49 @@ $$; COMMENT ON FUNCTION public.author_of_concept(public.my_concepts) IS 'Computed one-to-one: returns the PlatformAccount which authored a given Concept.'; +CREATE OR REPLACE FUNCTION public.rid_to_space_id_and_local_id(rid VARCHAR) +RETURNS public.accessible_resource STRICT STABLE +SET search_path = '' +LANGUAGE plpgsql AS $$ +DECLARE + uri VARCHAR; + source_local_id VARCHAR; + source_id BIGINT; +BEGIN +source_local_id := split_part(rid, '/', -1); +IF length(source_local_id) = length(rid) THEN + RETURN (null, 'Not a Rid')::public.accessible_resource; +END IF; +uri := substr(rid, 1, length(rid) - length(source_local_id) - 1); +IF rid ~ '^orn:\w+\.\w+:.*$' THEN + uri := concat(split_part(split_part(uri, ':', 2), '.', 1), ':', split_part(uri, ':', 3)); +ELSE + IF rid ~ '^orn:\w+:.*$' THEN + uri := substr(uri, 5); + END IF; +END IF; +SELECT id INTO source_id FROM public."Space" where url=uri; +IF source_id IS NULL THEN + RETURN (null, concat('Cannot find ', uri))::public.accessible_resource; +END IF; +RETURN (source_id, source_local_id); +END; +$$; + +CREATE OR REPLACE FUNCTION public.rid_or_local_id_to_concept_db_id(rid VARCHAR, default_space_id BIGINT) +RETURNS BIGINT STRICT STABLE +SET search_path = '' +LANGUAGE plpgsql AS $$ +DECLARE r public.accessible_resource; +BEGIN + r := (SELECT public.rid_to_space_id_and_local_id(rid)); + IF r.space_id IS NULL THEN + RETURN (SELECT id FROM public."Concept" WHERE space_id = default_space_id AND source_local_id = rid); + ELSE + RETURN (SELECT id FROM public."Concept" WHERE space_id = r.space_id AND source_local_id = r.source_local_id); + END IF; +END; +$$; CREATE TYPE public.concept_local_input AS ( -- concept columns @@ -292,37 +335,34 @@ BEGIN SELECT id FROM public."Space" WHERE url = data.space_url INTO concept.space_id; END IF; - IF data.schema_represented_by_local_id IS NOT NULL THEN - SELECT cpt.id FROM public."Concept" cpt - WHERE cpt.source_local_id = data.schema_represented_by_local_id - AND cpt.space_id = concept.space_id INTO concept.schema_id; - END IF; IF concept.source_local_id = '' THEN concept.source_local_id := NULL; END IF; IF data.represented_by_local_id = '' THEN data.represented_by_local_id := NULL; END IF; + IF data.schema_represented_by_local_id IS NOT NULL THEN + SELECT public.rid_or_local_id_to_concept_db_id( + data.schema_represented_by_local_id, concept.space_id) INTO concept.schema_id; + END IF; concept.source_local_id = COALESCE(concept.source_local_id, data.represented_by_local_id); -- legacy input field + concept.reference_content := coalesce(data.reference_content, '{}'::jsonb); IF data.local_reference_content IS NOT NULL THEN FOR key, value IN SELECT * FROM jsonb_each(data.local_reference_content) LOOP IF jsonb_typeof(value) = 'array' THEN WITH el AS (SELECT jsonb_array_elements_text(value) as x), - ela AS (SELECT array_agg(x) AS a FROM el) - SELECT array_agg(DISTINCT cpt.id) INTO STRICT ref_array_val - FROM public."Concept" AS cpt - JOIN ela ON (true) WHERE cpt.source_local_id = ANY(ela.a) AND cpt.space_id=concept.space_id; + el2 AS (SELECT public.rid_or_local_id_to_concept_db_id(x, concept.space_id) AS id FROM el) + SELECT array_agg(DISTINCT el2.id) INTO STRICT ref_array_val + FROM el2 WHERE el2.id IS NOT NULL; reference_content := jsonb_set(reference_content, ARRAY[key], to_jsonb(ref_array_val)); ELSIF jsonb_typeof(value) = 'string' THEN - SELECT cpt.id INTO STRICT ref_single_val - FROM public."Concept" AS cpt - WHERE cpt.source_local_id = (value #>> '{}') AND cpt.space_id=concept.space_id; + SELECT public.rid_or_local_id_to_concept_db_id(value #>> '{}', concept.space_id) INTO STRICT ref_single_val; reference_content := jsonb_set(reference_content, ARRAY[key], to_jsonb(ref_single_val)); ELSE RAISE EXCEPTION 'Invalid value in local_reference_content % %', value, jsonb_typeof(value); END IF; END LOOP; - SELECT reference_content INTO concept.reference_content; + concept.reference_content := concept.reference_content || reference_content; END IF; RETURN concept; END;