From e5af729fb4ca9a91ac304208ee6bf2ab88afa390 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Wed, 20 May 2026 13:19:26 +0200 Subject: [PATCH] TypeSchema: collect referenced nested types into SnapshotProfileTypeSchema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds snapshot.nested by closure rather than just the profile's own nested array. Walks: - Profile-local nested types (seeded first — they take precedence by URL over inherited copies when constraints exist). - Each nested type's fields (transitive nested-in-nested closure). - The profile's own constrained fields. - The base resource's fields — inherited types referenced by base fields matter to consumers that read inherited slots, and flatProfile only carries profile-level constraints. Identities are preserved (same URLs); nested schemas are shared by reference, not re-rooted to the profile. Currently sets up data with no immediate consumer — the TS writer still imports nested types via tsIndex.resolveType — but future code reading methods/types from the snapshot directly no longer needs to round-trip through the index for nested lookups. --- src/typeschema/utils.ts | 35 ++++++++- test/unit/typeschema/utils.test.ts | 109 +++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index d1741f19..40ce22b8 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -598,8 +598,41 @@ export const mkTypeSchemaIndex = ( }; }; + /** Walk fields recursively and collect every nested type they reference. Profile-local + * nested types (already in `into`) take precedence over inherited ones — their URLs + * are seeded first, so the inherited lookup is skipped for them. Identities (URLs) are + * preserved; this is a closure-by-reference, not a re-rooting. */ + const collectReferencedNested = ( + fields: Record | undefined, + into: Map, + ): void => { + if (!fields) return; + for (const field of Object.values(fields)) { + if (isChoiceDeclarationField(field)) continue; + const t = field.type; + if (!t || !isNestedIdentifier(t) || into.has(t.url)) continue; + const nested = resolveType(t); + if (!isNestedTypeSchema(nested)) continue; + into.set(t.url, nested); + collectReferencedNested(nested.fields, into); + } + }; + const buildProfileSnapshot = (schema: ProfileTypeSchema): SnapshotProfileTypeSchema => { const flat = flatProfile(schema); + const collected = new Map(); + // Profile-local nested types take precedence by URL — they may carry constraints + // not present on the inherited copy. + for (const n of flat.nested ?? []) collected.set(n.identifier.url, n); + for (const n of flat.nested ?? []) collectReferencedNested(n.fields, collected); + collectReferencedNested(flat.fields, collected); + // Profile fields only carry constraints; consumers also access inherited base fields, + // which may reference nested types invisible from the profile's own constraints. + const baseSchema = resolveType(flat.base); + if (baseSchema && "fields" in baseSchema) { + collectReferencedNested(baseSchema.fields, collected); + } + const nested = collected.size > 0 ? Array.from(collected.values()) : undefined; return { identifier: snapshotIdentifier(flat.identifier), base: flat.base, @@ -607,7 +640,7 @@ export const mkTypeSchemaIndex = ( fields: flat.fields ?? {}, extensions: flat.extensions, dependencies: flat.dependencies, - nested: flat.nested, + nested, }; }; diff --git a/test/unit/typeschema/utils.test.ts b/test/unit/typeschema/utils.test.ts index 8d44ff1a..859358fe 100644 --- a/test/unit/typeschema/utils.test.ts +++ b/test/unit/typeschema/utils.test.ts @@ -2,11 +2,13 @@ import { describe, expect, it } from "bun:test"; import type { CanonicalUrl, Name, + NestedTypeSchema, ProfileTypeSchema, RegularField, SpecializationTypeSchema, TypeIdentifier, } from "@typeschema/types"; +import { snapshotIdentifier } from "@typeschema/types"; import { mkTypeSchemaIndex } from "@typeschema/utils"; const stringType: TypeIdentifier = { @@ -604,4 +606,111 @@ describe("TypeSchema Index", () => { expect(result.identifier).toEqual(constraintSchema.identifier); }); }); + + describe("snapshot nested collection", () => { + it("inherits nested types referenced by inherited fields from the base", () => { + const nestedId = { + name: "Patient_contact" as Name, + package: "test", + kind: "nested" as const, + version: "1.0.0", + url: "http://example.org/StructureDefinition/Patient#contact" as CanonicalUrl, + }; + + const nestedSchema: NestedTypeSchema = { + identifier: nestedId, + base: stringType, + fields: { + name: { type: stringType, required: false, array: false } as RegularField, + }, + }; + + const baseSchema: SpecializationTypeSchema = { + identifier: { + name: "Patient" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Patient" as CanonicalUrl, + }, + fields: { + contact: { type: nestedId, required: false, array: true } as RegularField, + }, + nested: [nestedSchema], + }; + + const profileSchema: ProfileTypeSchema = { + identifier: { + name: "MyPatient" as Name, + package: "test", + kind: "profile", + version: "1.0.0", + url: "http://example.org/StructureDefinition/MyPatient" as CanonicalUrl, + }, + base: baseSchema.identifier, + fields: {}, + }; + + const index = mkTypeSchemaIndex([baseSchema, profileSchema], {}); + const snapshot = index.resolve(snapshotIdentifier(profileSchema.identifier)); + + expect(snapshot).toBeDefined(); + expect(snapshot?.nested?.map((n) => n.identifier.url)).toEqual([nestedId.url]); + }); + + it("prefers profile-local nested over inherited when URLs match", () => { + const nestedUrl = "http://example.org/StructureDefinition/Patient#contact" as CanonicalUrl; + const baseNested: NestedTypeSchema = { + identifier: { + name: "Patient_contact" as Name, + package: "test", + kind: "nested", + version: "1.0.0", + url: nestedUrl, + }, + base: stringType, + fields: { name: { type: stringType } as RegularField }, + }; + const profileNested: NestedTypeSchema = { + identifier: baseNested.identifier, + base: stringType, + // Profile-local version constrains the field as required. + fields: { name: { type: stringType, required: true } as RegularField }, + }; + + const baseSchema: SpecializationTypeSchema = { + identifier: { + name: "Patient" as Name, + package: "test", + kind: "resource", + version: "1.0.0", + url: "http://example.org/StructureDefinition/Patient" as CanonicalUrl, + }, + fields: { + contact: { type: baseNested.identifier, required: false, array: true } as RegularField, + }, + nested: [baseNested], + }; + + const profileSchema: ProfileTypeSchema = { + identifier: { + name: "MyPatient" as Name, + package: "test", + kind: "profile", + version: "1.0.0", + url: "http://example.org/StructureDefinition/MyPatient" as CanonicalUrl, + }, + base: baseSchema.identifier, + fields: {}, + nested: [profileNested], + }; + + const index = mkTypeSchemaIndex([baseSchema, profileSchema], {}); + const snapshot = index.resolve(snapshotIdentifier(profileSchema.identifier)); + + expect(snapshot?.nested).toHaveLength(1); + const collected = snapshot?.nested?.[0]; + expect((collected?.fields?.name as RegularField).required).toBe(true); + }); + }); });