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
31 changes: 12 additions & 19 deletions apps/obsidian/src/utils/importNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "./importRelations";
import { createTemplateFile } from "./templates";
import { resolveFolderForSpaceUri } from "./importFolderMetadata";
import { buildSchemaRid, findLocalNodeTypeMatch } from "./schemaMatching";

type PublishedNode = {
source_local_id: string;
Expand Down Expand Up @@ -1067,20 +1068,13 @@ export const mapNodeTypeIdToLocal = async ({

const schemaName = schemaData.name;

// Prefer match by node type ID (imported type may already exist locally with same id)
const matchById = plugin.settings.nodeTypes.find(
(nt) => nt.id === sourceNodeTypeId,
);
if (matchById) {
return matchById.id;
}

// Fall back to match by name
const matchingLocalNodeType = plugin.settings.nodeTypes.find(
(nt) => nt.name === schemaName,
);
if (matchingLocalNodeType) {
return matchingLocalNodeType.id;
const localMatch = findLocalNodeTypeMatch({
localNodeTypes: plugin.settings.nodeTypes,
id: sourceNodeTypeId,
name: schemaName,
});
if (localMatch) {
return localMatch.id;
}

// No matching local nodeType: create one from literal_content and add to settings
Expand All @@ -1090,11 +1084,10 @@ export const mapNodeTypeIdToLocal = async ({
);

const now = new Date().getTime();
const importedFromRid = spaceUriAndLocalIdToRid(
sourceSpaceUri,
sourceNodeTypeId,
"schema",
);
const importedFromRid = buildSchemaRid({
spaceUri: sourceSpaceUri,
localId: sourceNodeTypeId,
});

const newNodeType: DiscourseNode = {
id: sourceNodeTypeId,
Expand Down
48 changes: 23 additions & 25 deletions apps/obsidian/src/utils/importRelations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {
} from "./relationsStore";
import { DEFAULT_TLDRAW_COLOR } from "./tldrawColors";
import { mapNodeTypeIdToLocal } from "./importNodes";
import {
buildSchemaRid,
findExistingTriple,
findLocalRelationTypeMatch,
} from "./schemaMatching";

type ConceptInRelation = {
id: number;
Expand Down Expand Up @@ -66,29 +71,22 @@ const mapRelationTypeToLocal = async ({
const label = (obj.label as string) || schemaData.name;
const complement = (obj.complement as string) || "";

// Match by id first; if id exists locally with different label/complement, use local
const matchById = plugin.settings.relationTypes.find(
(rt) => rt.id === sourceRelationTypeId,
);
if (matchById) {
return matchById.id;
}

// Match by label
const matchByLabel = plugin.settings.relationTypes.find(
(rt) => rt.label === label,
);
if (matchByLabel) {
return matchByLabel.id;
// A local match wins even when label/complement differ — local wording is authoritative
const localMatch = findLocalRelationTypeMatch({
localRelationTypes: plugin.settings.relationTypes,
id: sourceRelationTypeId,
label,
});
if (localMatch) {
return localMatch.id;
}

// Create new relation type
const now = new Date().getTime();
const importedFromRid = spaceUriAndLocalIdToRid(
sourceSpaceUri,
sourceRelationTypeId,
"schema",
);
const importedFromRid = buildSchemaRid({
spaceUri: sourceSpaceUri,
localId: sourceRelationTypeId,
});

const newRelationType: DiscourseRelationType = {
id: sourceRelationTypeId,
Expand Down Expand Up @@ -133,12 +131,12 @@ const findOrCreateTriple = async ({
importedFromRid?: string;
authorId?: number;
}): Promise<DiscourseRelation> => {
const existing = plugin.settings.discourseRelations?.find(
(dr) =>
dr.sourceId === sourceNodeTypeId &&
dr.destinationId === destNodeTypeId &&
dr.relationshipTypeId === relationTypeId,
);
const existing = findExistingTriple({
discourseRelations: plugin.settings.discourseRelations ?? [],
sourceId: sourceNodeTypeId,
destinationId: destNodeTypeId,
relationshipTypeId: relationTypeId,
});
if (existing) return existing;

const now = Date.now();
Expand Down
95 changes: 95 additions & 0 deletions apps/obsidian/src/utils/schemaMatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid";
import type {
DiscourseNode,
DiscourseRelation,
DiscourseRelationType,
} from "~/types";

/**
* Shared matching primitives for the two schema import paths: importing from a
* remote Supabase space, and importing from an exported schema file. Both need
* to answer "does this incoming type already exist locally?" the same way, or
* the same vault reached through the two paths would dedupe differently.
*/

export const normalizeSchemaLabel = (value: string): string => {
return value.trim().toLowerCase();
};

/**
* Match by id first: an id collision means the type came from the same origin,
* which is stronger evidence than a name that two vaults happen to share.
*/
export const findLocalNodeTypeMatch = ({
localNodeTypes,
id,
name,
}: {
localNodeTypes: DiscourseNode[];
id: string;
name: string;
}): DiscourseNode | undefined => {
const matchById = localNodeTypes.find((nodeType) => nodeType.id === id);
if (matchById) return matchById;

const normalizedName = normalizeSchemaLabel(name);
return localNodeTypes.find(
(nodeType) => normalizeSchemaLabel(nodeType.name) === normalizedName,
);
};

export const findLocalRelationTypeMatch = ({
localRelationTypes,
id,
label,
}: {
localRelationTypes: DiscourseRelationType[];
id: string;
label: string;
}): DiscourseRelationType | undefined => {
const matchById = localRelationTypes.find(
(relationType) => relationType.id === id,
);
if (matchById) return matchById;

const normalizedLabel = normalizeSchemaLabel(label);
return localRelationTypes.find(
(relationType) =>
normalizeSchemaLabel(relationType.label) === normalizedLabel,
);
};

/**
* A discourse relation is identified by its endpoints and relation type, not by
* its own id — the id is regenerated per vault, so two vaults describing the
* same triple hold different ids for it.
*/
export const findExistingTriple = ({
discourseRelations,
sourceId,
destinationId,
relationshipTypeId,
}: {
discourseRelations: DiscourseRelation[];
sourceId: string;
destinationId: string;
relationshipTypeId: string;
}): DiscourseRelation | undefined => {
return discourseRelations.find(
(relation) =>
relation.sourceId === sourceId &&
relation.destinationId === destinationId &&
relation.relationshipTypeId === relationshipTypeId,
);
};

/** Pins the "schema" RID subtype so both import paths produce identical RIDs. */
export const buildSchemaRid = ({
spaceUri,
localId,
}: {
spaceUri: string;
localId: string;
}): string => {
return spaceUriAndLocalIdToRid(spaceUri, localId, "schema");
};
Loading