Skip to content
Open
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
98 changes: 98 additions & 0 deletions packages/database/src/lib/crossAppConverters.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {
LocalRef,
Ref,
LocalOrRemoteRef,
CrossAppEmbedding,
InlineCrossAppContent,
CrossAppBase,
CrossAppNode,
CrossAppRelation,
} from "../crossAppContracts";
import { LocalContentDataInput, LocalConceptDataInput } from "../inputTypes";
import { Enums, CompositeTypes } from "../dbTypes";
import { spaceUriAndLocalIdToRid, ridToSpaceUriAndLocalId } from "./rid";

type InlineEmbeddingInput = CompositeTypes<"inline_embedding_input">;
type InlineAbstractBase = Partial<CrossAppBase>;
Expand Down Expand Up @@ -39,6 +42,62 @@ const decodeRef = <DbVarName extends string, LocalVarName extends string>(
return decodeLocalRef(ref, localVarName);
};

const decodeLocalOrRemoteRef = <
LocalVarName extends string,
DbVarName extends string,
>({
ref,
dbVarName,
localVarName,
currentSpaceUri,
}: {
ref: LocalOrRemoteRef | InlineAbstractBase | undefined;
dbVarName: DbVarName;
localVarName: LocalVarName;
currentSpaceUri: string;
}):
| Record<LocalVarName, string>
| Record<DbVarName, number>
| Record<string, never> => {
if (ref === undefined) return {};
if ("dbId" in ref)
return { [dbVarName]: ref.dbId } as Record<DbVarName, number>;
if ("rid" in ref && ref.rid !== undefined) {
const { sourceLocalId, spaceUri } = ridToSpaceUriAndLocalId(ref.rid);
if (spaceUri === currentSpaceUri)
return {
[localVarName]: sourceLocalId,
} as Record<LocalVarName, string>;
return {
[localVarName]: ref.rid,
} as Record<LocalVarName, string>;
}
if ("space" in ref && ref.space !== undefined) {
if ("dbId" in ref.space) {
// not sure how to handle this
return {
[localVarName]: ref.localId,
} as Record<LocalVarName, string>;
}
if ("url" in ref.space) {
if (ref.space.url === currentSpaceUri) {
return {
[localVarName]: ref.localId,
} as Record<LocalVarName, string>;
} else
return {
[localVarName]: spaceUriAndLocalIdToRid(ref.space.url, ref.localId),
} as Record<LocalVarName, string>;
}
}
if ("localId" in ref) {
return {
[localVarName]: ref.localId,
} as Record<LocalVarName, string>;
}
return {};
};

const crossAppEmbeddingToDbEmbedding = (
embedding: CrossAppEmbedding | undefined,
): InlineEmbeddingInput | undefined =>
Expand Down Expand Up @@ -109,3 +168,42 @@ export const crossAppNodeToDbConcept = (
last_modified: node.modifiedAt?.toISOString(),
});
};

export const crossAppRelationToDbConcept = (
node: CrossAppRelation,
currentSpaceUri: string,
): LocalConceptDataInput => {
const relData = {
...decodeLocalOrRemoteRef({
ref: node.source,
dbVarName: "source",
localVarName: "source_local_id",
currentSpaceUri,
}),
...decodeLocalOrRemoteRef({
ref: node.destination,
dbVarName: "destination",
localVarName: "destination_local_id",
currentSpaceUri,
}),
};
const refLocalIds = Object.fromEntries(
Object.entries(relData)
.filter(([k]) => k.endsWith("_local_id"))
.map(([k, v]) => [k.substring(0, k.length - 9), v]),
) as Record<string, string>;
const refIds = Object.fromEntries(
Object.entries(relData).filter(
([k]) => k.endsWith("_id") && !k.endsWith("_local_id"),
),
) as Record<string, number>;
Comment on lines +176 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Database IDs for relation source and destination are silently dropped

The relation's source and destination database IDs are stored under keys that don't match the filter (dbVarName: "source" and "destination" at packages/database/src/lib/crossAppConverters.ts:179,185), so the subsequent filter for keys ending in _id never captures them.

Impact: When a relation's source or destination is referenced by database ID, the reference is silently lost and the resulting concept has empty reference_content.

Key mismatch between dbVarName and refIds filter logic

decodeLocalOrRemoteRef is called with dbVarName: "source" (line 179) and dbVarName: "destination" (line 185). When the ref is a DbRef, the function returns e.g. { source: 42 } (packages/database/src/lib/crossAppConverters.ts:64).

Then refIds at line 195-199 filters relData entries for keys ending in "_id" but not "_local_id". Since "source" and "destination" don't end in "_id", they are filtered out. The numeric database IDs are never included in the reference_content output at line 205.

The fix is to change the dbVarName values to "source_id" and "destination_id" so they match the _id suffix filter, and then strip the _id suffix when building refIds (similar to how refLocalIds strips _local_id).

Prompt for agents
In crossAppRelationToDbConcept (packages/database/src/lib/crossAppConverters.ts:172-209), the dbVarName values passed to decodeLocalOrRemoteRef are "source" and "destination", but the refIds filter at lines 195-199 looks for keys ending in "_id" (excluding "_local_id"). Since "source" and "destination" don't end in "_id", database IDs are silently dropped.

Two approaches to fix:
1. Change dbVarName to "source_id" and "destination_id", then in the refIds construction, strip the "_id" suffix from keys (like refLocalIds strips "_local_id") so the final reference_content JSON has keys "source" and "destination" with numeric values.
2. Alternatively, change the refIds filter to check for entries that are numbers rather than relying on key naming conventions.

Approach 1 is more consistent with the existing pattern. The refIds block would need a .map step like: .map(([k, v]) => [k.substring(0, k.length - 3), v]) to strip the "_id" suffix.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +195 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter logic will never capture source or destination dbIds. The decodeLocalOrRemoteRef function returns dbVarNames as "source" and "destination" (lines 179, 185), which don't end with "_id". This filter checks for keys ending with "_id" but not "_local_id", so it will never match "source" or "destination".

This means when source/destination are passed as dbIds (via the "dbId" property path on lines 63-64), they will be lost and not included in reference_content.

Fix: Change the dbVarNames to include the _id suffix:

// In decodeLocalOrRemoteRef calls:
dbVarName: "source_id",  // instead of "source"
dbVarName: "destination_id",  // instead of "destination"

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


return filterUndefined<LocalConceptDataInput>({
...decodeLocalRef(node, "source_local_id"),
...decodeRef(node.author, "author_id", "author_local_id"),
local_reference_content: refLocalIds,
reference_content: refIds,
created: node.createdAt?.toISOString(),
last_modified: node.modifiedAt?.toISOString(),
});
Comment on lines +201 to +208

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Relation type is not included when converting a relation to a database concept

The relation's type is not mapped to any database field (crossAppRelationToDbConcept at packages/database/src/lib/crossAppConverters.ts:201-208), unlike the parallel node converter which maps node type to the schema fields.

Impact: Relations stored in the database will have no schema/type association, making it impossible to distinguish different kinds of relations.

Missing relationType mapping compared to crossAppNodeToDbConcept

In crossAppNodeToDbConcept (packages/database/src/lib/crossAppConverters.ts:155-170), node.nodeType is mapped via decodeRef(node.nodeType, "schema_id", "schema_represented_by_local_id") at line 162. The CrossAppRelation type has an analogous relationType: Ref field (packages/database/src/crossAppContracts.ts:113), but crossAppRelationToDbConcept never maps it. The concept_local_input type has schema_id and schema_represented_by_local_id fields that should receive this value.

Suggested change
return filterUndefined<LocalConceptDataInput>({
...decodeLocalRef(node, "source_local_id"),
...decodeRef(node.author, "author_id", "author_local_id"),
local_reference_content: refLocalIds,
reference_content: refIds,
created: node.createdAt?.toISOString(),
last_modified: node.modifiedAt?.toISOString(),
});
return filterUndefined<LocalConceptDataInput>({
...decodeLocalRef(node, "source_local_id"),
...decodeRef(node.author, "author_id", "author_local_id"),
...decodeRef(node.relationType, "schema_id", "schema_represented_by_local_id"),
local_reference_content: refLocalIds,
reference_content: refIds,
created: node.createdAt?.toISOString(),
last_modified: node.modifiedAt?.toISOString(),
});
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

};