From b5dfb43b1bb1612151b3a1c65e1154e7f2e934cf Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:07:42 -0600 Subject: [PATCH] fix(fmodata): preserve useEntityIds through navigate() navigate() lost per-table useEntityIds after Database.from() mutation fix. Now passes resolved useEntityIds to new EntitySet and resolves navigation path names through resolveTableId for entity ID support. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-navigate-useentityids.md | 5 +++ packages/fmodata/src/client/entity-set.ts | 17 +++++++-- packages/fmodata/src/client/record-builder.ts | 5 ++- packages/fmodata/tests/navigate.test.ts | 36 ++++++++++++++++++- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-navigate-useentityids.md diff --git a/.changeset/fix-navigate-useentityids.md b/.changeset/fix-navigate-useentityids.md new file mode 100644 index 00000000..a1fcc3be --- /dev/null +++ b/.changeset/fix-navigate-useentityids.md @@ -0,0 +1,5 @@ +--- +"@proofkit/fmodata": patch +--- + +Fix navigate() losing per-table useEntityIds after Database.from() mutation fix diff --git a/packages/fmodata/src/client/entity-set.ts b/packages/fmodata/src/client/entity-set.ts index 2cac622b..727755b1 100644 --- a/packages/fmodata/src/client/entity-set.ts +++ b/packages/fmodata/src/client/entity-set.ts @@ -10,6 +10,7 @@ import type { } from "../orm/table"; import { FMTable as FMTableClass, getDefaultSelect, getTableColumns, getTableName, getTableSchema } from "../orm/table"; import type { ExecutionContext } from "../types"; +import { resolveTableId } from "./builders/table-utils"; import type { Database } from "./database"; import { DeleteBuilder } from "./delete-builder"; import { InsertBuilder } from "./insert-builder"; @@ -361,12 +362,22 @@ export class EntitySet, DatabaseIncludeSpecialColu databaseName: this.databaseName, context: this.context, database: this.database, + useEntityIds: this.databaseUseEntityIds, }); + // Resolve navigation names using entity IDs when appropriate + const resolvedRelation = resolveTableId(targetTable, relationName, this.context, this.databaseUseEntityIds); + const resolvedSourceName = resolveTableId( + this.occurrence, + getTableName(this.occurrence), + this.context, + this.databaseUseEntityIds, + ); + // Store the navigation info in the EntitySet // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern (entitySet as any).isNavigateFromEntitySet = true; // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern - (entitySet as any).navigateRelation = relationName; + (entitySet as any).navigateRelation = resolvedRelation; // Build the full base path for chained navigations if (this.isNavigateFromEntitySet && this.navigateBasePath) { @@ -382,9 +393,9 @@ export class EntitySet, DatabaseIncludeSpecialColu // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern (entitySet as any).navigateSourceTableName = this.navigateSourceTableName; } else { - // Initial navigation - source is just the table name + // Initial navigation - source is just the table name (resolved to entity ID if needed) // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern - (entitySet as any).navigateSourceTableName = getTableName(this.occurrence); + (entitySet as any).navigateSourceTableName = resolvedSourceName; } return entitySet; } diff --git a/packages/fmodata/src/client/record-builder.ts b/packages/fmodata/src/client/record-builder.ts index be65a851..5a2e7fb0 100644 --- a/packages/fmodata/src/client/record-builder.ts +++ b/packages/fmodata/src/client/record-builder.ts @@ -445,9 +445,8 @@ export class RecordBuilder< databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns, }); - // Store the navigation info - we'll use it in execute - // Use relation name as-is (entity ID handling is done in QueryBuilder) - const relationId = relationName; + // Store the navigation info - resolve entity ID for relation if needed + const relationId = resolveTableId(targetTable, relationName, this.context, this.databaseUseEntityIds); // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path let sourceTableName: string; diff --git a/packages/fmodata/tests/navigate.test.ts b/packages/fmodata/tests/navigate.test.ts index a038d8ad..50f69eaf 100644 --- a/packages/fmodata/tests/navigate.test.ts +++ b/packages/fmodata/tests/navigate.test.ts @@ -7,7 +7,16 @@ import { dateField, fmTableOccurrence, textField } from "@proofkit/fmodata"; import { describe, expect, expectTypeOf, it } from "vitest"; -import { arbitraryTable, contacts, createMockClient, invoices, lineItems, users } from "./utils/test-setup"; +import { + arbitraryTable, + contacts, + contactsTOWithIds, + createMockClient, + invoices, + lineItems, + users, + usersTOWithIds, +} from "./utils/test-setup"; const contactsUsersPathRegex = /^\/contacts\/users/; @@ -179,6 +188,31 @@ describe("navigate", () => { // }); }); + it("should preserve useEntityIds from parent EntitySet through navigate", () => { + const db = client.database("test_db"); + const query = db.from(contactsTOWithIds).navigate(usersTOWithIds).list(); + const qs = query.getQueryString(); + + // Should use FMTID for table names, not field names + expect(qs).toContain("FMTID:200"); // contacts table entity ID + expect(qs).toContain("FMTID:1065093"); // users table entity ID + expect(qs).not.toContain("/contacts/"); + expect(qs).not.toContain("/users?"); + }); + + it("should preserve useEntityIds through record navigate", () => { + const db = client.database("test_db"); + const qs = db + .from(contactsTOWithIds) + .get("rec-1") + .navigate(usersTOWithIds) + .select({ name: usersTOWithIds.name }) + .getQueryString(); + + expect(qs).toContain("FMTID:200"); // contacts source + expect(qs).toContain("FMTID:1065093"); // users relation + }); + // Issue #107: navigate() doesn't include parent table in URL path // when defaultSelect is "schema" or an object describe("with defaultSelect='schema' (#107)", () => {