Skip to content

Commit ee12033

Browse files
committed
fix: install @types/node stub for tsc compilation + add contained ref validation
- Install minimal @types/node stub (declares 'module' built-in) before tsc compilation so generated Class files using createRequire() compile without TS2591 errors. Stub is removed after compilation. - Add generateContainedRefValidation() for standalone (non-Bundle) resources: verifies #fragment references resolve against Resource.contained[] entries. Uses same error message as HL7 validator for parity. - Wire containedRefValidation into both hasConstraints and no-constraints output paths (same pattern as extensionStructuralValidation). - Update validatorGaps test to expect contained ref check on non-Bundle types while still verifying Bundle-specific logic (fullUrls, urn:uuid) is absent. Closes #54
1 parent acd6a75 commit ee12033

4 files changed

Lines changed: 60 additions & 4 deletions

File tree

src/generator/emitters/validator/validatorGenerator.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
buildPrimitiveFormatValidations,
1919
} from './validatorFieldBuilders.js';
2020
import { buildBindingValidations } from './validatorBindingBuilder.js';
21-
import { generateBundleRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
21+
import { generateBundleRefValidation, generateContainedRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
2222

2323
const log = logger.withTag('validator');
2424

@@ -180,6 +180,7 @@ export function generateValidateProfileFunction(
180180
});
181181
const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
182182
const bundleRefValidation = generateBundleRefValidation(baseResourceType);
183+
const containedRefValidation = generateContainedRefValidation(baseResourceType);
183184
const extensionStructuralValidation = generateExtensionStructuralValidation();
184185

185186
// ── Deduplicate & filter constraints ────────────────────────────────────
@@ -274,7 +275,7 @@ export async function validate${interfaceName}(resource: ${interfaceName}, optio
274275
const errors: string[] = [];
275276
const warnings: string[] = [];
276277
void options;
277-
${extensionStructuralValidation}
278+
${extensionStructuralValidation}${containedRefValidation}
278279
return { errors, warnings };
279280
}`,
280281
valueSetImports
@@ -323,7 +324,7 @@ ${extensionStructuralValidation}
323324
const errors: string[] = [];
324325
const warnings: string[] = [];
325326
${fhirpathOptionsBlock}
326-
${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${bundleRefValidation}
327+
${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${containedRefValidation}${bundleRefValidation}
327328
return { errors, warnings };
328329
}`,
329330
valueSetImports

src/generator/emitters/validator/validatorTemplates.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,46 @@ export function generateBundleRefValidation(baseResourceType: string | undefined
5656
`;
5757
}
5858

59+
/**
60+
* Generate standalone contained reference resolution validation code.
61+
* For non-Bundle resources, verifies that #fragment references resolve to contained[] entries.
62+
* Bundle validation is handled separately by generateBundleRefValidation.
63+
*/
64+
export function generateContainedRefValidation(baseResourceType: string | undefined): string {
65+
// Bundle resources have their own reference resolution check; skip them here
66+
if (baseResourceType === 'Bundle') return '';
67+
return `
68+
// Standalone contained reference resolution: verify #id references resolve to contained[] entries
69+
{
70+
const _res = resource as unknown as Record<string, unknown>;
71+
const _containedIds = new Set<string>();
72+
if (Array.isArray(_res.contained)) {
73+
for (const _c of _res.contained as Array<Record<string, unknown>>) {
74+
if (_c && typeof _c.id === 'string') _containedIds.add(_c.id);
75+
}
76+
}
77+
const _checkContainedRef = (obj: unknown): void => {
78+
if (!obj || typeof obj !== 'object') return;
79+
if (Array.isArray(obj)) { obj.forEach(_checkContainedRef); return; }
80+
const _rec = obj as Record<string, unknown>;
81+
if (typeof _rec.reference === 'string') {
82+
const _ref = _rec.reference as string;
83+
if (_ref.startsWith('#')) {
84+
const _id = _ref.substring(1);
85+
if (_id && !_containedIds.has(_id)) {
86+
errors.push('Bundled or contained reference not found within the bundle/resource ' + _ref);
87+
}
88+
}
89+
}
90+
for (const [_k, _v] of Object.entries(_rec)) {
91+
if (_k !== 'contained') _checkContainedRef(_v);
92+
}
93+
};
94+
_checkContainedRef(_res);
95+
}
96+
`;
97+
}
98+
5999
/**
60100
* Generate extension structural validation code.
61101
* Checks extension.url required, ext-1 constraint, empty objects.

src/generator/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,24 @@ export async function generateIntoPackage(packageArchivePath: string, outArchive
423423
const { installFhirpathStub, removeFhirpathStub } = await import('./emitters/validator/fhirpathStubInstaller.js');
424424
installFhirpathStub(outputDir);
425425

426+
// Install minimal @types/node stub so tsc can resolve `import { createRequire } from 'module'`
427+
const nodeTypesDir = path.join(outputDir, 'node_modules', '@types', 'node');
428+
fs.mkdirSync(nodeTypesDir, { recursive: true });
429+
fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'),
430+
`declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
431+
fs.writeFileSync(path.join(nodeTypesDir, 'package.json'),
432+
JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
433+
426434
// Compile TypeScript to JavaScript
427435
logger.log('Compiling TypeScript to JavaScript...');
428436
await compileTypeScriptToJS(outputDir);
429437

430438
// Remove fhirpath stub — the real package is a peer dependency
431439
removeFhirpathStub(outputDir);
432440

441+
// Remove @types/node stub — only needed for compilation
442+
try { fs.rmSync(path.join(outputDir, 'node_modules', '@types'), { recursive: true, force: true }); } catch { /* ignore */ }
443+
433444
// Remove base client type stubs — they were only needed for tsc to resolve
434445
// @babelfhir-ts/client-<version> imports during compilation. The real package is
435446
// installed by the consumer via npm.

src/test/validatorGaps.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,11 @@ describe('validatorGenerator gaps', () => {
476476
];
477477
const code = generateCode('TestPatient', fields, { baseResourceType: 'Patient' });
478478

479-
expect(code).not.toContain('Bundled or contained reference not found');
479+
// Non-Bundle types should not have the Bundle-specific entry/fullUrl resolution logic
480+
expect(code).not.toContain('_fullUrls');
481+
expect(code).not.toContain('urn:uuid:');
482+
// But they SHOULD have standalone contained reference resolution (#id checks)
483+
expect(code).toContain('_checkContainedRef');
480484
});
481485
});
482486

0 commit comments

Comments
 (0)