@atomic-ehr/codegen 0.0.1-canary.20250808232913.e62fbdc → 0.0.1-canary.20250810124254.c6a6c21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/builder.d.ts +13 -1
- package/dist/api/builder.d.ts.map +1 -1
- package/dist/api/generators/rest-client.d.ts +115 -0
- package/dist/api/generators/rest-client.d.ts.map +1 -0
- package/dist/api/generators/search-parameter-enhancer.d.ts +187 -0
- package/dist/api/generators/search-parameter-enhancer.d.ts.map +1 -0
- package/dist/api/generators/types.d.ts +54 -0
- package/dist/api/generators/types.d.ts.map +1 -0
- package/dist/api/generators/typescript.d.ts +44 -0
- package/dist/api/generators/typescript.d.ts.map +1 -1
- package/dist/api/generators/validation-generator.d.ts +127 -0
- package/dist/api/generators/validation-generator.d.ts.map +1 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/index.js +5 -2
- package/dist/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/{index-k2baxg01.js → index-t2hbzagh.js} +3113 -282
- package/dist/index.js +5 -1
- package/dist/typeschema/core/transformer.d.ts +6 -2
- package/dist/typeschema/core/transformer.d.ts.map +1 -1
- package/dist/typeschema/profile/processor.d.ts +2 -2
- package/dist/typeschema/profile/processor.d.ts.map +1 -1
- package/dist/typeschema/types.d.ts +171 -1
- package/dist/typeschema/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1733,10 +1733,16 @@ async function transformProfile(fhirSchema, manager, packageInfo) {
|
|
|
1733
1733
|
if (fhirSchema.description) {
|
|
1734
1734
|
profileSchema.description = fhirSchema.description;
|
|
1735
1735
|
}
|
|
1736
|
-
const metadata = extractProfileMetadata(fhirSchema);
|
|
1736
|
+
const metadata = extractProfileMetadata(fhirSchema, packageInfo);
|
|
1737
1737
|
if (Object.keys(metadata).length > 0) {
|
|
1738
1738
|
profileSchema.metadata = metadata;
|
|
1739
1739
|
}
|
|
1740
|
+
if (fhirSchema.elements) {
|
|
1741
|
+
const fields = await transformElements(fhirSchema, [], fhirSchema.elements, manager, packageInfo);
|
|
1742
|
+
if (Object.keys(fields).length > 0) {
|
|
1743
|
+
profileSchema.fields = fields;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1740
1746
|
const constraints = await processProfileConstraints(fhirSchema, manager);
|
|
1741
1747
|
if (Object.keys(constraints).length > 0) {
|
|
1742
1748
|
profileSchema.constraints = constraints;
|
|
@@ -1766,15 +1772,12 @@ async function determineBaseKind(baseUrl, manager) {
|
|
|
1766
1772
|
} catch (error) {
|
|
1767
1773
|
console.warn(`Could not resolve base schema ${baseUrl}:`, error);
|
|
1768
1774
|
}
|
|
1769
|
-
if (baseUrl.includes("/us/core/") || baseUrl.includes("StructureDefinition/us-core-")) {
|
|
1770
|
-
return "profile";
|
|
1771
|
-
}
|
|
1772
1775
|
if (baseUrl.includes("StructureDefinition/") && !baseUrl.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
|
|
1773
1776
|
return "profile";
|
|
1774
1777
|
}
|
|
1775
1778
|
return "resource";
|
|
1776
1779
|
}
|
|
1777
|
-
function extractProfileMetadata(fhirSchema) {
|
|
1780
|
+
function extractProfileMetadata(fhirSchema, packageInfo) {
|
|
1778
1781
|
const metadata = {};
|
|
1779
1782
|
if (fhirSchema.publisher)
|
|
1780
1783
|
metadata.publisher = fhirSchema.publisher;
|
|
@@ -1791,12 +1794,10 @@ function extractProfileMetadata(fhirSchema) {
|
|
|
1791
1794
|
metadata.date = fhirSchema.date;
|
|
1792
1795
|
if (fhirSchema.jurisdiction)
|
|
1793
1796
|
metadata.jurisdiction = fhirSchema.jurisdiction;
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
metadata.package = "hl7.fhir.r4.core";
|
|
1799
|
-
}
|
|
1797
|
+
if (packageInfo?.name) {
|
|
1798
|
+
metadata.package = packageInfo.name;
|
|
1799
|
+
} else if (fhirSchema.package_name) {
|
|
1800
|
+
metadata.package = fhirSchema.package_name;
|
|
1800
1801
|
}
|
|
1801
1802
|
return metadata;
|
|
1802
1803
|
}
|
|
@@ -1809,7 +1810,7 @@ async function processProfileConstraints(fhirSchema, _manager) {
|
|
|
1809
1810
|
if (element.min !== undefined)
|
|
1810
1811
|
elementConstraints.min = element.min;
|
|
1811
1812
|
if (element.max !== undefined)
|
|
1812
|
-
elementConstraints.max = element.max;
|
|
1813
|
+
elementConstraints.max = String(element.max);
|
|
1813
1814
|
if (element.mustSupport)
|
|
1814
1815
|
elementConstraints.mustSupport = true;
|
|
1815
1816
|
if (element.fixedValue !== undefined)
|
|
@@ -1819,7 +1820,7 @@ async function processProfileConstraints(fhirSchema, _manager) {
|
|
|
1819
1820
|
if (element.binding) {
|
|
1820
1821
|
elementConstraints.binding = {
|
|
1821
1822
|
strength: element.binding.strength,
|
|
1822
|
-
valueSet: element.binding.valueSet
|
|
1823
|
+
valueSet: element.binding.valueSet ?? ""
|
|
1823
1824
|
};
|
|
1824
1825
|
}
|
|
1825
1826
|
if (element.type && Array.isArray(element.type) && element.type.length > 0) {
|
|
@@ -1834,8 +1835,8 @@ async function processProfileConstraints(fhirSchema, _manager) {
|
|
|
1834
1835
|
}
|
|
1835
1836
|
if (element.slicing) {
|
|
1836
1837
|
elementConstraints.slicing = {
|
|
1837
|
-
discriminator: element.slicing.discriminator,
|
|
1838
|
-
rules: element.slicing
|
|
1838
|
+
discriminator: element.slicing.discriminator ?? [],
|
|
1839
|
+
rules: String(element.slicing),
|
|
1839
1840
|
ordered: element.slicing.ordered
|
|
1840
1841
|
};
|
|
1841
1842
|
}
|
|
@@ -1857,7 +1858,7 @@ async function processProfileExtensions(fhirSchema, _manager) {
|
|
|
1857
1858
|
path: path9,
|
|
1858
1859
|
profile: type.profile,
|
|
1859
1860
|
min: element.min,
|
|
1860
|
-
max: element.max,
|
|
1861
|
+
max: String(element.max),
|
|
1861
1862
|
mustSupport: element.mustSupport
|
|
1862
1863
|
});
|
|
1863
1864
|
}
|
|
@@ -2951,203 +2952,2340 @@ var isRegularField = (field) => {
|
|
|
2951
2952
|
var isTypeSchemaForResourceComplexTypeLogical = (schema) => {
|
|
2952
2953
|
return schema.identifier.kind === "resource" || schema.identifier.kind === "complex-type" || schema.identifier.kind === "logical";
|
|
2953
2954
|
};
|
|
2954
|
-
// src/api/generators/
|
|
2955
|
+
// src/api/generators/rest-client.ts
|
|
2955
2956
|
import { mkdir as mkdir2, writeFile as writeFile4 } from "node:fs/promises";
|
|
2956
2957
|
import { dirname, join as join10 } from "node:path";
|
|
2957
2958
|
|
|
2958
|
-
// src/
|
|
2959
|
-
|
|
2960
|
-
const parts = input.replace(/[^A-Za-z0-9]+/g, " ").split(" ").map((p) => p.trim()).filter(Boolean);
|
|
2961
|
-
if (parts.length === 0)
|
|
2962
|
-
return "";
|
|
2963
|
-
return parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
// src/api/generators/typescript.ts
|
|
2967
|
-
var PRIMITIVE_TYPE_MAP = {
|
|
2968
|
-
string: "string",
|
|
2969
|
-
code: "string",
|
|
2970
|
-
uri: "string",
|
|
2971
|
-
url: "string",
|
|
2972
|
-
canonical: "string",
|
|
2973
|
-
oid: "string",
|
|
2974
|
-
uuid: "string",
|
|
2975
|
-
id: "string",
|
|
2976
|
-
markdown: "string",
|
|
2977
|
-
xhtml: "string",
|
|
2978
|
-
base64Binary: "string",
|
|
2979
|
-
integer: "number",
|
|
2980
|
-
unsignedInt: "number",
|
|
2981
|
-
positiveInt: "number",
|
|
2982
|
-
decimal: "number",
|
|
2983
|
-
boolean: "boolean",
|
|
2984
|
-
date: "string",
|
|
2985
|
-
dateTime: "string",
|
|
2986
|
-
instant: "string",
|
|
2987
|
-
time: "string"
|
|
2988
|
-
};
|
|
2989
|
-
|
|
2990
|
-
class TypeScriptAPIGenerator {
|
|
2991
|
-
options;
|
|
2992
|
-
imports = new Map;
|
|
2993
|
-
exports = new Set;
|
|
2959
|
+
// src/api/generators/search-parameter-enhancer.ts
|
|
2960
|
+
class SearchParameterEnhancer {
|
|
2994
2961
|
resourceTypes = new Set;
|
|
2995
|
-
|
|
2962
|
+
resourceSearchParams = new Map;
|
|
2963
|
+
autocompleteEnabled;
|
|
2964
|
+
valueSetEnumsEnabled;
|
|
2965
|
+
availableEnumTypes = new Map;
|
|
2966
|
+
static BASE_PARAM_NAMES = [
|
|
2967
|
+
"_count",
|
|
2968
|
+
"_offset",
|
|
2969
|
+
"_sort",
|
|
2970
|
+
"_summary",
|
|
2971
|
+
"_elements",
|
|
2972
|
+
"_lastUpdated",
|
|
2973
|
+
"_profile",
|
|
2974
|
+
"_security",
|
|
2975
|
+
"_tag",
|
|
2976
|
+
"_id",
|
|
2977
|
+
"_text",
|
|
2978
|
+
"_content"
|
|
2979
|
+
];
|
|
2996
2980
|
constructor(options) {
|
|
2997
|
-
this.
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
2981
|
+
this.autocompleteEnabled = !!options?.autocomplete;
|
|
2982
|
+
this.valueSetEnumsEnabled = !!options?.valueSetEnums;
|
|
2983
|
+
console.log(`[DEBUG] SearchParameterEnhancer initialized: autocomplete=${this.autocompleteEnabled}, valueSetEnums=${this.valueSetEnumsEnabled}`);
|
|
2984
|
+
}
|
|
2985
|
+
generateSearchParamNameUnions() {
|
|
2986
|
+
const baseUnion = SearchParameterEnhancer.BASE_PARAM_NAMES.map((n) => `'${n}'`).join(" | ");
|
|
2987
|
+
const parts = [];
|
|
2988
|
+
parts.push(`/**
|
|
2989
|
+
* Base search parameter names available for all resources
|
|
2990
|
+
*/`);
|
|
2991
|
+
parts.push(`export type BaseSearchParamName = ${baseUnion};`);
|
|
2992
|
+
parts.push("");
|
|
2993
|
+
const resourceTypesArray = Array.from(this.resourceTypes).sort();
|
|
2994
|
+
for (const [resourceType, params] of this.resourceSearchParams.entries()) {
|
|
2995
|
+
const specificNames = Array.from(new Set(params.map((p) => p.name))).sort();
|
|
2996
|
+
const specificUnion = specificNames.map((n) => `'${n}'`).join(" | ");
|
|
2997
|
+
const typeName = `${resourceType}SearchParamName`;
|
|
2998
|
+
if (specificUnion.length > 0) {
|
|
2999
|
+
parts.push(`/**
|
|
3000
|
+
* Search parameter names for ${resourceType}
|
|
3001
|
+
*/`);
|
|
3002
|
+
parts.push(`export type ${typeName} = BaseSearchParamName | ${specificUnion};`);
|
|
3003
|
+
} else {
|
|
3004
|
+
parts.push(`export type ${typeName} = BaseSearchParamName;`);
|
|
3005
|
+
}
|
|
3006
|
+
parts.push("");
|
|
3011
3007
|
}
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
exports,
|
|
3023
|
-
filename
|
|
3024
|
-
};
|
|
3008
|
+
const mappingLines = resourceTypesArray.map((t) => ` T extends '${t}' ? ${t}SearchParamName :`).join(`
|
|
3009
|
+
`);
|
|
3010
|
+
parts.push(`/**
|
|
3011
|
+
* Generic search parameter name union for a given resource type
|
|
3012
|
+
*/`);
|
|
3013
|
+
parts.push(`export type SearchParamName<T extends ResourceTypes> =
|
|
3014
|
+
${mappingLines}
|
|
3015
|
+
BaseSearchParamName;`);
|
|
3016
|
+
return parts.join(`
|
|
3017
|
+
`);
|
|
3025
3018
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
const baseInterface = this.getBaseInterface(schema);
|
|
3035
|
-
if (baseInterface && !baseInterface.isPrimitive) {
|
|
3036
|
-
this.imports.set(baseInterface.value, baseInterface.value);
|
|
3037
|
-
lines.push(`export interface ${overridedName ?? interfaceName} extends ${baseInterface.value} {`);
|
|
3038
|
-
} else {
|
|
3039
|
-
lines.push(`export interface ${overridedName ?? interfaceName} {`);
|
|
3019
|
+
collectResourceData(schemas) {
|
|
3020
|
+
this.resourceTypes.clear();
|
|
3021
|
+
this.resourceSearchParams.clear();
|
|
3022
|
+
for (const schema of schemas) {
|
|
3023
|
+
if (schema.identifier.kind === "resource" && schema.identifier.name !== "DomainResource" && schema.identifier.name !== "Resource") {
|
|
3024
|
+
this.resourceTypes.add(schema.identifier.name);
|
|
3025
|
+
this.collectSearchParameters(schema);
|
|
3026
|
+
}
|
|
3040
3027
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3028
|
+
}
|
|
3029
|
+
collectSearchParameters(schema) {
|
|
3030
|
+
const resourceType = schema.identifier.name;
|
|
3031
|
+
const searchParams = [];
|
|
3032
|
+
this.addCommonSearchParameters(resourceType, searchParams);
|
|
3033
|
+
this.resourceSearchParams.set(resourceType, searchParams);
|
|
3034
|
+
}
|
|
3035
|
+
addCommonSearchParameters(resourceType, searchParams) {
|
|
3036
|
+
switch (resourceType) {
|
|
3037
|
+
case "Patient":
|
|
3038
|
+
searchParams.push({
|
|
3039
|
+
name: "active",
|
|
3040
|
+
type: "token",
|
|
3041
|
+
description: "Whether the patient record is active"
|
|
3042
|
+
}, {
|
|
3043
|
+
name: "address",
|
|
3044
|
+
type: "string",
|
|
3045
|
+
description: "A server defined search that may match any of the string fields in the Address"
|
|
3046
|
+
}, {
|
|
3047
|
+
name: "address-city",
|
|
3048
|
+
type: "string",
|
|
3049
|
+
description: "A city specified in an address"
|
|
3050
|
+
}, {
|
|
3051
|
+
name: "address-country",
|
|
3052
|
+
type: "string",
|
|
3053
|
+
description: "A country specified in an address"
|
|
3054
|
+
}, {
|
|
3055
|
+
name: "address-postalcode",
|
|
3056
|
+
type: "string",
|
|
3057
|
+
description: "A postalCode specified in an address"
|
|
3058
|
+
}, {
|
|
3059
|
+
name: "address-state",
|
|
3060
|
+
type: "string",
|
|
3061
|
+
description: "A state specified in an address"
|
|
3062
|
+
}, {
|
|
3063
|
+
name: "address-use",
|
|
3064
|
+
type: "token",
|
|
3065
|
+
description: "A use code specified in an address"
|
|
3066
|
+
}, {
|
|
3067
|
+
name: "birthdate",
|
|
3068
|
+
type: "date",
|
|
3069
|
+
description: "The patient's date of birth"
|
|
3070
|
+
}, {
|
|
3071
|
+
name: "death-date",
|
|
3072
|
+
type: "date",
|
|
3073
|
+
description: "The date of death has been provided and satisfies this search value"
|
|
3074
|
+
}, {
|
|
3075
|
+
name: "deceased",
|
|
3076
|
+
type: "token",
|
|
3077
|
+
description: "This patient has been marked as deceased, or as a death date entered"
|
|
3078
|
+
}, {
|
|
3079
|
+
name: "email",
|
|
3080
|
+
type: "token",
|
|
3081
|
+
description: "A value in an email contact"
|
|
3082
|
+
}, {
|
|
3083
|
+
name: "family",
|
|
3084
|
+
type: "string",
|
|
3085
|
+
description: "A portion of the family name of the patient"
|
|
3086
|
+
}, {
|
|
3087
|
+
name: "gender",
|
|
3088
|
+
type: "token",
|
|
3089
|
+
description: "Gender of the patient"
|
|
3090
|
+
}, {
|
|
3091
|
+
name: "general-practitioner",
|
|
3092
|
+
type: "reference",
|
|
3093
|
+
target: ["Organization", "Practitioner", "PractitionerRole"],
|
|
3094
|
+
description: "Patient's nominated general practitioner"
|
|
3095
|
+
}, {
|
|
3096
|
+
name: "given",
|
|
3097
|
+
type: "string",
|
|
3098
|
+
description: "A portion of the given name of the patient"
|
|
3099
|
+
}, {
|
|
3100
|
+
name: "identifier",
|
|
3101
|
+
type: "token",
|
|
3102
|
+
description: "A patient identifier"
|
|
3103
|
+
}, {
|
|
3104
|
+
name: "language",
|
|
3105
|
+
type: "token",
|
|
3106
|
+
description: "Language code (irrespective of use value)"
|
|
3107
|
+
}, {
|
|
3108
|
+
name: "link",
|
|
3109
|
+
type: "reference",
|
|
3110
|
+
target: ["Patient", "RelatedPerson"],
|
|
3111
|
+
description: "All patients linked to the given patient"
|
|
3112
|
+
}, {
|
|
3113
|
+
name: "name",
|
|
3114
|
+
type: "string",
|
|
3115
|
+
description: "A server defined search that may match any of the string fields in the HumanName"
|
|
3116
|
+
}, {
|
|
3117
|
+
name: "organization",
|
|
3118
|
+
type: "reference",
|
|
3119
|
+
target: ["Organization"],
|
|
3120
|
+
description: "The organization that is the custodian of the patient record"
|
|
3121
|
+
}, {
|
|
3122
|
+
name: "phone",
|
|
3123
|
+
type: "token",
|
|
3124
|
+
description: "A value in a phone contact"
|
|
3125
|
+
}, {
|
|
3126
|
+
name: "phonetic",
|
|
3127
|
+
type: "string",
|
|
3128
|
+
description: "A portion of either family or given name using some kind of phonetic matching algorithm"
|
|
3129
|
+
}, {
|
|
3130
|
+
name: "telecom",
|
|
3131
|
+
type: "token",
|
|
3132
|
+
description: "The value in any kind of telecom details of the patient"
|
|
3133
|
+
});
|
|
3134
|
+
break;
|
|
3135
|
+
case "Observation":
|
|
3136
|
+
searchParams.push({
|
|
3137
|
+
name: "category",
|
|
3138
|
+
type: "token",
|
|
3139
|
+
description: "The classification of the type of observation"
|
|
3140
|
+
}, {
|
|
3141
|
+
name: "code",
|
|
3142
|
+
type: "token",
|
|
3143
|
+
description: "The code of the observation type"
|
|
3144
|
+
}, {
|
|
3145
|
+
name: "component-code",
|
|
3146
|
+
type: "token",
|
|
3147
|
+
description: "The component code of the observation type"
|
|
3148
|
+
}, {
|
|
3149
|
+
name: "component-data-absent-reason",
|
|
3150
|
+
type: "token",
|
|
3151
|
+
description: "The reason why the expected value in the element Observation.component.value[x] is missing"
|
|
3152
|
+
}, {
|
|
3153
|
+
name: "component-value-concept",
|
|
3154
|
+
type: "token",
|
|
3155
|
+
description: "The value of the component observation, if the value is a CodeableConcept"
|
|
3156
|
+
}, {
|
|
3157
|
+
name: "component-value-quantity",
|
|
3158
|
+
type: "quantity",
|
|
3159
|
+
description: "The value of the component observation, if the value is a Quantity, or a SampledData"
|
|
3160
|
+
}, {
|
|
3161
|
+
name: "data-absent-reason",
|
|
3162
|
+
type: "token",
|
|
3163
|
+
description: "The reason why the expected value in the element Observation.value[x] is missing"
|
|
3164
|
+
}, {
|
|
3165
|
+
name: "date",
|
|
3166
|
+
type: "date",
|
|
3167
|
+
description: "Obtained date/time. If the obtained element is a period, a date that falls in the period"
|
|
3168
|
+
}, {
|
|
3169
|
+
name: "derived-from",
|
|
3170
|
+
type: "reference",
|
|
3171
|
+
target: [
|
|
3172
|
+
"DocumentReference",
|
|
3173
|
+
"ImagingStudy",
|
|
3174
|
+
"Media",
|
|
3175
|
+
"QuestionnaireResponse",
|
|
3176
|
+
"Observation",
|
|
3177
|
+
"MolecularSequence"
|
|
3178
|
+
],
|
|
3179
|
+
description: "Related measurements the observation is made from"
|
|
3180
|
+
}, {
|
|
3181
|
+
name: "device",
|
|
3182
|
+
type: "reference",
|
|
3183
|
+
target: ["Device", "DeviceMetric"],
|
|
3184
|
+
description: "The Device that generated the observation data"
|
|
3185
|
+
}, {
|
|
3186
|
+
name: "encounter",
|
|
3187
|
+
type: "reference",
|
|
3188
|
+
target: ["Encounter"],
|
|
3189
|
+
description: "Encounter related to the observation"
|
|
3190
|
+
}, {
|
|
3191
|
+
name: "focus",
|
|
3192
|
+
type: "reference",
|
|
3193
|
+
target: ["Resource"],
|
|
3194
|
+
description: "The focus of an observation when the focus is not the patient of record"
|
|
3195
|
+
}, {
|
|
3196
|
+
name: "has-member",
|
|
3197
|
+
type: "reference",
|
|
3198
|
+
target: [
|
|
3199
|
+
"Observation",
|
|
3200
|
+
"QuestionnaireResponse",
|
|
3201
|
+
"MolecularSequence"
|
|
3202
|
+
],
|
|
3203
|
+
description: "Related resource that belongs to the Observation group"
|
|
3204
|
+
}, {
|
|
3205
|
+
name: "identifier",
|
|
3206
|
+
type: "token",
|
|
3207
|
+
description: "The unique id for a particular observation"
|
|
3208
|
+
}, {
|
|
3209
|
+
name: "method",
|
|
3210
|
+
type: "token",
|
|
3211
|
+
description: "The method used for the observation"
|
|
3212
|
+
}, {
|
|
3213
|
+
name: "part-of",
|
|
3214
|
+
type: "reference",
|
|
3215
|
+
target: [
|
|
3216
|
+
"MedicationAdministration",
|
|
3217
|
+
"MedicationDispense",
|
|
3218
|
+
"MedicationStatement",
|
|
3219
|
+
"Procedure",
|
|
3220
|
+
"Immunization",
|
|
3221
|
+
"ImagingStudy"
|
|
3222
|
+
],
|
|
3223
|
+
description: "Part of referenced event"
|
|
3224
|
+
}, {
|
|
3225
|
+
name: "patient",
|
|
3226
|
+
type: "reference",
|
|
3227
|
+
target: ["Patient"],
|
|
3228
|
+
description: "The subject that the observation is about (if patient)"
|
|
3229
|
+
}, {
|
|
3230
|
+
name: "performer",
|
|
3231
|
+
type: "reference",
|
|
3232
|
+
target: [
|
|
3233
|
+
"Practitioner",
|
|
3234
|
+
"PractitionerRole",
|
|
3235
|
+
"Organization",
|
|
3236
|
+
"CareTeam",
|
|
3237
|
+
"Patient",
|
|
3238
|
+
"RelatedPerson"
|
|
3239
|
+
],
|
|
3240
|
+
description: "Who performed the observation"
|
|
3241
|
+
}, {
|
|
3242
|
+
name: "specimen",
|
|
3243
|
+
type: "reference",
|
|
3244
|
+
target: ["Specimen"],
|
|
3245
|
+
description: "Specimen used for this observation"
|
|
3246
|
+
}, {
|
|
3247
|
+
name: "status",
|
|
3248
|
+
type: "token",
|
|
3249
|
+
description: "The status of the observation"
|
|
3250
|
+
}, {
|
|
3251
|
+
name: "subject",
|
|
3252
|
+
type: "reference",
|
|
3253
|
+
target: ["Patient", "Group", "Device", "Location"],
|
|
3254
|
+
description: "The subject that the observation is about"
|
|
3255
|
+
}, {
|
|
3256
|
+
name: "value-concept",
|
|
3257
|
+
type: "token",
|
|
3258
|
+
description: "The value of the observation, if the value is a CodeableConcept"
|
|
3259
|
+
}, {
|
|
3260
|
+
name: "value-date",
|
|
3261
|
+
type: "date",
|
|
3262
|
+
description: "The value of the observation, if the value is a date or period of time"
|
|
3263
|
+
}, {
|
|
3264
|
+
name: "value-quantity",
|
|
3265
|
+
type: "quantity",
|
|
3266
|
+
description: "The value of the observation, if the value is a Quantity, or a SampledData"
|
|
3267
|
+
}, {
|
|
3268
|
+
name: "value-string",
|
|
3269
|
+
type: "string",
|
|
3270
|
+
description: "The value of the observation, if the value is a string, and also searches in CodeableConcept.text"
|
|
3271
|
+
});
|
|
3272
|
+
break;
|
|
3273
|
+
case "Organization":
|
|
3274
|
+
searchParams.push({
|
|
3275
|
+
name: "active",
|
|
3276
|
+
type: "token",
|
|
3277
|
+
description: "Is the Organization record active"
|
|
3278
|
+
}, {
|
|
3279
|
+
name: "address",
|
|
3280
|
+
type: "string",
|
|
3281
|
+
description: "A server defined search that may match any of the string fields in the Address"
|
|
3282
|
+
}, {
|
|
3283
|
+
name: "address-city",
|
|
3284
|
+
type: "string",
|
|
3285
|
+
description: "A city specified in an address"
|
|
3286
|
+
}, {
|
|
3287
|
+
name: "address-country",
|
|
3288
|
+
type: "string",
|
|
3289
|
+
description: "A country specified in an address"
|
|
3290
|
+
}, {
|
|
3291
|
+
name: "address-postalcode",
|
|
3292
|
+
type: "string",
|
|
3293
|
+
description: "A postal code specified in an address"
|
|
3294
|
+
}, {
|
|
3295
|
+
name: "address-state",
|
|
3296
|
+
type: "string",
|
|
3297
|
+
description: "A state specified in an address"
|
|
3298
|
+
}, {
|
|
3299
|
+
name: "address-use",
|
|
3300
|
+
type: "token",
|
|
3301
|
+
description: "A use code specified in an address"
|
|
3302
|
+
}, {
|
|
3303
|
+
name: "endpoint",
|
|
3304
|
+
type: "reference",
|
|
3305
|
+
target: ["Endpoint"],
|
|
3306
|
+
description: "Technical endpoints providing access to services operated for the organization"
|
|
3307
|
+
}, {
|
|
3308
|
+
name: "identifier",
|
|
3309
|
+
type: "token",
|
|
3310
|
+
description: "Any identifier for the organization (not the accreditation issuer's identifier)"
|
|
3311
|
+
}, {
|
|
3312
|
+
name: "name",
|
|
3313
|
+
type: "string",
|
|
3314
|
+
description: "A portion of the organization's name or alias"
|
|
3315
|
+
}, {
|
|
3316
|
+
name: "partof",
|
|
3317
|
+
type: "reference",
|
|
3318
|
+
target: ["Organization"],
|
|
3319
|
+
description: "An organization of which this organization forms a part"
|
|
3320
|
+
}, {
|
|
3321
|
+
name: "phonetic",
|
|
3322
|
+
type: "string",
|
|
3323
|
+
description: "A portion of the organization's name using some kind of phonetic matching algorithm"
|
|
3324
|
+
}, {
|
|
3325
|
+
name: "type",
|
|
3326
|
+
type: "token",
|
|
3327
|
+
description: "A code for the type of organization"
|
|
3328
|
+
});
|
|
3329
|
+
break;
|
|
3330
|
+
case "Practitioner":
|
|
3331
|
+
searchParams.push({
|
|
3332
|
+
name: "active",
|
|
3333
|
+
type: "token",
|
|
3334
|
+
description: "Whether the practitioner record is active"
|
|
3335
|
+
}, {
|
|
3336
|
+
name: "address",
|
|
3337
|
+
type: "string",
|
|
3338
|
+
description: "A server defined search that may match any of the string fields in the Address"
|
|
3339
|
+
}, {
|
|
3340
|
+
name: "address-city",
|
|
3341
|
+
type: "string",
|
|
3342
|
+
description: "A city specified in an address"
|
|
3343
|
+
}, {
|
|
3344
|
+
name: "address-country",
|
|
3345
|
+
type: "string",
|
|
3346
|
+
description: "A country specified in an address"
|
|
3347
|
+
}, {
|
|
3348
|
+
name: "address-postalcode",
|
|
3349
|
+
type: "string",
|
|
3350
|
+
description: "A postal code specified in an address"
|
|
3351
|
+
}, {
|
|
3352
|
+
name: "address-state",
|
|
3353
|
+
type: "string",
|
|
3354
|
+
description: "A state specified in an address"
|
|
3355
|
+
}, {
|
|
3356
|
+
name: "address-use",
|
|
3357
|
+
type: "token",
|
|
3358
|
+
description: "A use code specified in an address"
|
|
3359
|
+
}, {
|
|
3360
|
+
name: "communication",
|
|
3361
|
+
type: "token",
|
|
3362
|
+
description: "One of the languages that the practitioner can communicate with"
|
|
3363
|
+
}, {
|
|
3364
|
+
name: "email",
|
|
3365
|
+
type: "token",
|
|
3366
|
+
description: "A value in an email contact"
|
|
3367
|
+
}, {
|
|
3368
|
+
name: "family",
|
|
3369
|
+
type: "string",
|
|
3370
|
+
description: "A portion of the family name"
|
|
3371
|
+
}, {
|
|
3372
|
+
name: "gender",
|
|
3373
|
+
type: "token",
|
|
3374
|
+
description: "Gender of the practitioner"
|
|
3375
|
+
}, {
|
|
3376
|
+
name: "given",
|
|
3377
|
+
type: "string",
|
|
3378
|
+
description: "A portion of the given name"
|
|
3379
|
+
}, {
|
|
3380
|
+
name: "identifier",
|
|
3381
|
+
type: "token",
|
|
3382
|
+
description: "A practitioner's Identifier"
|
|
3383
|
+
}, {
|
|
3384
|
+
name: "name",
|
|
3385
|
+
type: "string",
|
|
3386
|
+
description: "A server defined search that may match any of the string fields in the HumanName"
|
|
3387
|
+
}, {
|
|
3388
|
+
name: "phone",
|
|
3389
|
+
type: "token",
|
|
3390
|
+
description: "A value in a phone contact"
|
|
3391
|
+
}, {
|
|
3392
|
+
name: "phonetic",
|
|
3393
|
+
type: "string",
|
|
3394
|
+
description: "A portion of either family or given name using some kind of phonetic matching algorithm"
|
|
3395
|
+
}, {
|
|
3396
|
+
name: "telecom",
|
|
3397
|
+
type: "token",
|
|
3398
|
+
description: "The value in any kind of contact"
|
|
3399
|
+
});
|
|
3400
|
+
break;
|
|
3401
|
+
default:
|
|
3402
|
+
searchParams.push({
|
|
3403
|
+
name: "identifier",
|
|
3404
|
+
type: "token",
|
|
3405
|
+
description: "Resource identifier"
|
|
3406
|
+
});
|
|
3407
|
+
break;
|
|
3044
3408
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3409
|
+
}
|
|
3410
|
+
generateValueSetUnionTypes() {
|
|
3411
|
+
return `/**
|
|
3412
|
+
* Curated ValueSet unions (string literal types)
|
|
3413
|
+
*/
|
|
3414
|
+
export type PatientGender = 'male' | 'female' | 'other' | 'unknown';
|
|
3415
|
+
export type ObservationStatus = 'registered' | 'preliminary' | 'final' | 'amended' | 'corrected' | 'cancelled' | 'entered-in-error' | 'unknown';
|
|
3416
|
+
export type ImmunizationStatus = 'completed' | 'entered-in-error' | 'not-done';`;
|
|
3417
|
+
}
|
|
3418
|
+
preprocessEnumTypes() {
|
|
3419
|
+
this.availableEnumTypes.clear();
|
|
3420
|
+
for (const [
|
|
3421
|
+
resourceType,
|
|
3422
|
+
searchParams
|
|
3423
|
+
] of this.resourceSearchParams.entries()) {
|
|
3424
|
+
for (const param of searchParams) {
|
|
3425
|
+
if (param.type === "token" && this.valueSetEnumsEnabled) {
|
|
3426
|
+
const enumTypeName = `${resourceType}${this.toPascalCase(param.name)}Values`;
|
|
3427
|
+
this.availableEnumTypes.set(`${resourceType}${param.name}`, enumTypeName);
|
|
3062
3428
|
}
|
|
3063
3429
|
}
|
|
3064
|
-
} else {
|
|
3065
|
-
lines.push("}");
|
|
3066
3430
|
}
|
|
3067
|
-
|
|
3431
|
+
}
|
|
3432
|
+
toPascalCase(str) {
|
|
3433
|
+
return str.split(/[-_\s]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
3434
|
+
}
|
|
3435
|
+
generateEnhancedSearchTypes() {
|
|
3436
|
+
const resourceTypesArray = Array.from(this.resourceTypes).sort();
|
|
3437
|
+
this.preprocessEnumTypes();
|
|
3438
|
+
const enumImports = this.valueSetEnumsEnabled && this.availableEnumTypes.size > 0 ? `import type { ${Array.from(new Set(this.availableEnumTypes.values())).sort().join(", ")} } from '../types/utility';
|
|
3439
|
+
` : "";
|
|
3440
|
+
return `/**
|
|
3441
|
+
* Enhanced Search Parameter Types
|
|
3442
|
+
*
|
|
3443
|
+
* Type-safe search parameters with modifiers and validation for FHIR resources.
|
|
3444
|
+
* Generated automatically from FHIR schemas.
|
|
3445
|
+
*/
|
|
3446
|
+
|
|
3447
|
+
import type { ResourceTypes } from '../types';
|
|
3448
|
+
${enumImports}
|
|
3449
|
+
|
|
3450
|
+
/**
|
|
3451
|
+
* Search parameter modifier types for enhanced type safety
|
|
3452
|
+
*/
|
|
3453
|
+
export interface SearchModifiers {
|
|
3454
|
+
/** String search modifiers */
|
|
3455
|
+
StringModifier:
|
|
3456
|
+
| { exact: string }
|
|
3457
|
+
| { contains: string }
|
|
3458
|
+
| { missing: boolean };
|
|
3459
|
+
|
|
3460
|
+
/** Date parameter with prefix support */
|
|
3461
|
+
DateParameter:
|
|
3462
|
+
| string
|
|
3463
|
+
| { gt: string }
|
|
3464
|
+
| { lt: string }
|
|
3465
|
+
| { ge: string }
|
|
3466
|
+
| { le: string }
|
|
3467
|
+
| { eq: string }
|
|
3468
|
+
| { ne: string }
|
|
3469
|
+
| { missing: boolean };
|
|
3470
|
+
|
|
3471
|
+
/** Token parameter for coded values */
|
|
3472
|
+
TokenParameter:
|
|
3473
|
+
| string
|
|
3474
|
+
| { system: string; code: string }
|
|
3475
|
+
| { code: string }
|
|
3476
|
+
| { system: string }
|
|
3477
|
+
| { missing: boolean };
|
|
3478
|
+
|
|
3479
|
+
/** Token search options (for better autocomplete when enum values exist) */
|
|
3480
|
+
TokenSearchOptions:
|
|
3481
|
+
| { system: string; code: string }
|
|
3482
|
+
| { code: string }
|
|
3483
|
+
| { system: string }
|
|
3484
|
+
| { missing: boolean };
|
|
3485
|
+
|
|
3486
|
+
/** Reference parameter for resource references */
|
|
3487
|
+
ReferenceParameter:
|
|
3488
|
+
| string
|
|
3489
|
+
| { reference: string }
|
|
3490
|
+
| { identifier: string }
|
|
3491
|
+
| { missing: boolean };
|
|
3492
|
+
|
|
3493
|
+
/** Number parameter with range support */
|
|
3494
|
+
NumberParameter:
|
|
3495
|
+
| number
|
|
3496
|
+
| { gt: number }
|
|
3497
|
+
| { lt: number }
|
|
3498
|
+
| { ge: number }
|
|
3499
|
+
| { le: number }
|
|
3500
|
+
| { eq: number }
|
|
3501
|
+
| { ne: number }
|
|
3502
|
+
| { missing: boolean };
|
|
3503
|
+
|
|
3504
|
+
/** Quantity parameter for measurements */
|
|
3505
|
+
QuantityParameter:
|
|
3506
|
+
| number
|
|
3507
|
+
| string
|
|
3508
|
+
| { value: number; unit?: string; system?: string; code?: string }
|
|
3509
|
+
| { missing: boolean };
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
/**
|
|
3513
|
+
* Base search parameters available for all resources
|
|
3514
|
+
*/
|
|
3515
|
+
export interface BaseEnhancedSearchParams {
|
|
3516
|
+
/** Number of results to return */
|
|
3517
|
+
_count?: number;
|
|
3518
|
+
/** Pagination offset */
|
|
3519
|
+
_offset?: number;
|
|
3520
|
+
/** Sort order */
|
|
3521
|
+
_sort?: string | string[];
|
|
3522
|
+
/** Summary mode */
|
|
3523
|
+
_summary?: 'true' | 'false' | 'text' | 'data' | 'count';
|
|
3524
|
+
/** Elements to include */
|
|
3525
|
+
_elements?: string | string[];
|
|
3526
|
+
/** Filter by last updated */
|
|
3527
|
+
_lastUpdated?: SearchModifiers['DateParameter'];
|
|
3528
|
+
/** Profile filter */
|
|
3529
|
+
_profile?: string | string[];
|
|
3530
|
+
/** Security label filter */
|
|
3531
|
+
_security?: string | string[];
|
|
3532
|
+
/** Tag filter */
|
|
3533
|
+
_tag?: SearchModifiers['TokenParameter'] | SearchModifiers['TokenParameter'][];
|
|
3534
|
+
/** Filter by ID */
|
|
3535
|
+
_id?: string | string[];
|
|
3536
|
+
/** Text search */
|
|
3537
|
+
_text?: string;
|
|
3538
|
+
/** Content search */
|
|
3539
|
+
_content?: string;
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
/**
|
|
3543
|
+
* Enhanced search parameters union type for all resources
|
|
3544
|
+
*/
|
|
3545
|
+
export type EnhancedSearchParams<T extends ResourceTypes> =
|
|
3546
|
+
${resourceTypesArray.map((type) => ` T extends '${type}' ? ${type}SearchParams :`).join(`
|
|
3547
|
+
`)}
|
|
3548
|
+
BaseEnhancedSearchParams;
|
|
3549
|
+
|
|
3550
|
+
${this.generateResourceSpecificSearchInterfaces()}
|
|
3551
|
+
|
|
3552
|
+
${this.autocompleteEnabled ? this.generateSearchParamNameUnions() : ""}
|
|
3553
|
+
|
|
3554
|
+
/**
|
|
3555
|
+
* Type-safe search parameter validation helpers
|
|
3556
|
+
*/
|
|
3557
|
+
export class SearchParameterValidator {
|
|
3558
|
+
/**
|
|
3559
|
+
* Validate search parameters for a specific resource type
|
|
3560
|
+
*/
|
|
3561
|
+
static validate<T extends ResourceTypes>(
|
|
3562
|
+
resourceType: T,
|
|
3563
|
+
params: EnhancedSearchParams<T>
|
|
3564
|
+
): { valid: boolean; errors: string[] } {
|
|
3565
|
+
const errors: string[] = [];
|
|
3566
|
+
|
|
3567
|
+
// Basic validation logic
|
|
3568
|
+
if (params._count !== undefined && (params._count < 0 || params._count > 1000)) {
|
|
3569
|
+
errors.push('_count must be between 0 and 1000');
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
if (params._offset !== undefined && params._offset < 0) {
|
|
3573
|
+
errors.push('_offset must be non-negative');
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
return {
|
|
3577
|
+
valid: errors.length === 0,
|
|
3578
|
+
errors
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
/**
|
|
3583
|
+
* Build URL search parameters from enhanced search params
|
|
3584
|
+
*/
|
|
3585
|
+
static buildSearchParams<T extends ResourceTypes>(
|
|
3586
|
+
resourceType: T,
|
|
3587
|
+
params: EnhancedSearchParams<T>
|
|
3588
|
+
): URLSearchParams {
|
|
3589
|
+
const searchParams = new URLSearchParams();
|
|
3590
|
+
|
|
3591
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3592
|
+
if (value === undefined || value === null) continue;
|
|
3593
|
+
|
|
3594
|
+
if (Array.isArray(value)) {
|
|
3595
|
+
value.forEach(v => searchParams.append(key, String(v)));
|
|
3596
|
+
} else if (typeof value === 'object') {
|
|
3597
|
+
// Handle complex parameter objects
|
|
3598
|
+
if ('exact' in value) {
|
|
3599
|
+
searchParams.append(key + ':exact', String(value.exact));
|
|
3600
|
+
} else if ('contains' in value) {
|
|
3601
|
+
searchParams.append(key + ':contains', String(value.contains));
|
|
3602
|
+
} else if ('missing' in value) {
|
|
3603
|
+
searchParams.append(key + ':missing', String(value.missing));
|
|
3604
|
+
} else if ('gt' in value) {
|
|
3605
|
+
searchParams.append(key + ':gt', String(value.gt));
|
|
3606
|
+
} else if ('lt' in value) {
|
|
3607
|
+
searchParams.append(key + ':lt', String(value.lt));
|
|
3608
|
+
} else if ('ge' in value) {
|
|
3609
|
+
searchParams.append(key + ':ge', String(value.ge));
|
|
3610
|
+
} else if ('le' in value) {
|
|
3611
|
+
searchParams.append(key + ':le', String(value.le));
|
|
3612
|
+
} else if ('eq' in value) {
|
|
3613
|
+
searchParams.append(key + ':eq', String(value.eq));
|
|
3614
|
+
} else if ('ne' in value) {
|
|
3615
|
+
searchParams.append(key + ':ne', String(value.ne));
|
|
3616
|
+
} else if ('system' in value && 'code' in value) {
|
|
3617
|
+
searchParams.append(key, \`\${value.system}|\${value.code}\`);
|
|
3618
|
+
} else if ('system' in value) {
|
|
3619
|
+
searchParams.append(key, \`\${value.system}|\`);
|
|
3620
|
+
} else if ('code' in value) {
|
|
3621
|
+
searchParams.append(key, \`|\${value.code}\`);
|
|
3622
|
+
} else if ('reference' in value) {
|
|
3623
|
+
searchParams.append(key, String(value.reference));
|
|
3624
|
+
} else if ('identifier' in value) {
|
|
3625
|
+
searchParams.append(key + ':identifier', String(value.identifier));
|
|
3626
|
+
}
|
|
3627
|
+
} else {
|
|
3628
|
+
searchParams.append(key, String(value));
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
return searchParams;
|
|
3633
|
+
}
|
|
3634
|
+
}`;
|
|
3635
|
+
}
|
|
3636
|
+
generateResourceSpecificSearchInterfaces() {
|
|
3637
|
+
const interfaces = [];
|
|
3638
|
+
for (const [
|
|
3639
|
+
resourceType,
|
|
3640
|
+
searchParams
|
|
3641
|
+
] of this.resourceSearchParams.entries()) {
|
|
3642
|
+
interfaces.push(this.generateResourceSearchInterface(resourceType, searchParams));
|
|
3643
|
+
}
|
|
3644
|
+
return interfaces.join(`
|
|
3645
|
+
|
|
3068
3646
|
`);
|
|
3069
3647
|
}
|
|
3070
|
-
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3648
|
+
generateResourceSearchInterface(resourceType, searchParams) {
|
|
3649
|
+
const interfaceFields = [];
|
|
3650
|
+
interfaceFields.push("\t// Base search parameters");
|
|
3651
|
+
interfaceFields.push("\t_count?: number;");
|
|
3652
|
+
interfaceFields.push("\t_offset?: number;");
|
|
3653
|
+
interfaceFields.push("\t_sort?: string | string[];");
|
|
3654
|
+
interfaceFields.push("\t_summary?: 'true' | 'false' | 'text' | 'data' | 'count';");
|
|
3655
|
+
interfaceFields.push("\t_elements?: string | string[];");
|
|
3656
|
+
interfaceFields.push("\t_lastUpdated?: SearchModifiers['DateParameter'];");
|
|
3657
|
+
interfaceFields.push("\t_profile?: string | string[];");
|
|
3658
|
+
interfaceFields.push("\t_security?: string | string[];");
|
|
3659
|
+
interfaceFields.push("\t_tag?: SearchModifiers['TokenParameter'] | SearchModifiers['TokenParameter'][];");
|
|
3660
|
+
interfaceFields.push("\t_id?: string | string[];");
|
|
3661
|
+
interfaceFields.push("\t_text?: string;");
|
|
3662
|
+
interfaceFields.push("\t_content?: string;");
|
|
3663
|
+
interfaceFields.push("");
|
|
3664
|
+
if (searchParams.length > 0) {
|
|
3665
|
+
interfaceFields.push(` // ${resourceType}-specific search parameters`);
|
|
3666
|
+
for (const param of searchParams) {
|
|
3667
|
+
const typeMapping = this.getTypeScriptTypeForSearchParameter(resourceType, param);
|
|
3668
|
+
const comment = param.description ? ` /** ${param.description} */` : "";
|
|
3669
|
+
interfaceFields.push(`${comment}`);
|
|
3670
|
+
interfaceFields.push(` '${param.name}'?: ${typeMapping};`);
|
|
3081
3671
|
}
|
|
3082
|
-
} else {
|
|
3083
|
-
lines.push(`export interface ${baseName}${interfaceName}{`);
|
|
3084
3672
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3673
|
+
return `/**
|
|
3674
|
+
* Enhanced search parameters for ${resourceType} resources
|
|
3675
|
+
*/
|
|
3676
|
+
export interface ${resourceType}SearchParams extends BaseEnhancedSearchParams {
|
|
3677
|
+
${interfaceFields.join(`
|
|
3678
|
+
`)}
|
|
3679
|
+
}`;
|
|
3680
|
+
}
|
|
3681
|
+
getTypeScriptTypeForSearchParameter(resourceType, param) {
|
|
3682
|
+
switch (param.type) {
|
|
3683
|
+
case "string":
|
|
3684
|
+
return "string | SearchModifiers['StringModifier']";
|
|
3685
|
+
case "number":
|
|
3686
|
+
return "number | SearchModifiers['NumberParameter']";
|
|
3687
|
+
case "date":
|
|
3688
|
+
return "string | SearchModifiers['DateParameter']";
|
|
3689
|
+
case "token":
|
|
3690
|
+
if (this.valueSetEnumsEnabled) {
|
|
3691
|
+
const enumTypeName = this.availableEnumTypes.get(`${resourceType}${param.name}`);
|
|
3692
|
+
if (enumTypeName) {
|
|
3693
|
+
return `${enumTypeName} | SearchModifiers['TokenSearchOptions']`;
|
|
3694
|
+
}
|
|
3090
3695
|
}
|
|
3091
|
-
|
|
3696
|
+
return "string | SearchModifiers['TokenParameter']";
|
|
3697
|
+
case "reference":
|
|
3698
|
+
if (param.target && param.target.length > 0) {
|
|
3699
|
+
return `string | SearchModifiers['ReferenceParameter']`;
|
|
3700
|
+
}
|
|
3701
|
+
return "string | SearchModifiers['ReferenceParameter']";
|
|
3702
|
+
case "quantity":
|
|
3703
|
+
return "number | string | SearchModifiers['QuantityParameter']";
|
|
3704
|
+
case "uri":
|
|
3705
|
+
return "string";
|
|
3706
|
+
case "composite":
|
|
3707
|
+
return "string";
|
|
3708
|
+
default:
|
|
3709
|
+
return "string";
|
|
3092
3710
|
}
|
|
3093
|
-
lines.push("}");
|
|
3094
|
-
return lines.join(`
|
|
3095
|
-
`);
|
|
3096
3711
|
}
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
return this.generatePolymorphicInstance(fieldName, field, nestedOpts);
|
|
3100
|
-
} else if (isRegularField(field)) {
|
|
3101
|
-
return this.generateRegularField(fieldName, field, nestedOpts);
|
|
3102
|
-
}
|
|
3103
|
-
return "";
|
|
3712
|
+
getResourceTypes() {
|
|
3713
|
+
return this.resourceTypes;
|
|
3104
3714
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
// src/api/generators/validation-generator.ts
|
|
3718
|
+
class ValidationGenerator {
|
|
3719
|
+
resourceTypes = new Set;
|
|
3720
|
+
resourceSchemas = new Map;
|
|
3721
|
+
collectResourceData(schemas) {
|
|
3722
|
+
this.resourceTypes.clear();
|
|
3723
|
+
this.resourceSchemas.clear();
|
|
3724
|
+
for (const schema of schemas) {
|
|
3725
|
+
if (schema.identifier.kind === "resource" && schema.identifier.name !== "DomainResource" && schema.identifier.name !== "Resource") {
|
|
3726
|
+
this.resourceTypes.add(schema.identifier.name);
|
|
3727
|
+
this.resourceSchemas.set(schema.identifier.name, schema);
|
|
3113
3728
|
}
|
|
3114
|
-
typeString = subType.value;
|
|
3115
3729
|
}
|
|
3116
|
-
const optional = !field.required ? "?" : "";
|
|
3117
|
-
const array = field.array ? "[]" : "";
|
|
3118
|
-
return `${fieldName}${optional}: ${nestedOpts?.isNested && nestedOpts.baseName ? nestedOpts.baseName : ""}${typeString}${array};`;
|
|
3119
3730
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3731
|
+
generateValidationTypes() {
|
|
3732
|
+
return `/**
|
|
3733
|
+
* FHIR Resource Validation Types
|
|
3734
|
+
*
|
|
3735
|
+
* Client-side validation types and interfaces for FHIR resources.
|
|
3736
|
+
* Generated automatically from FHIR schemas.
|
|
3737
|
+
*/
|
|
3738
|
+
|
|
3739
|
+
import type { ResourceTypes } from '../types';
|
|
3740
|
+
|
|
3741
|
+
/**
|
|
3742
|
+
* Validation options for resource validation
|
|
3743
|
+
*/
|
|
3744
|
+
export interface ValidationOptions {
|
|
3745
|
+
/** Validation profile to use (strict, lenient, etc.) */
|
|
3746
|
+
profile?: 'strict' | 'lenient' | 'minimal';
|
|
3747
|
+
/** Whether to throw on validation errors or return result */
|
|
3748
|
+
throwOnError?: boolean;
|
|
3749
|
+
/** Whether to validate required fields */
|
|
3750
|
+
validateRequired?: boolean;
|
|
3751
|
+
/** Whether to validate cardinality constraints */
|
|
3752
|
+
validateCardinality?: boolean;
|
|
3753
|
+
/** Whether to validate data types */
|
|
3754
|
+
validateTypes?: boolean;
|
|
3755
|
+
/** Whether to validate value constraints */
|
|
3756
|
+
validateConstraints?: boolean;
|
|
3757
|
+
/** Whether to collect performance metrics */
|
|
3758
|
+
collectMetrics?: boolean;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
/**
|
|
3762
|
+
* Validation error details
|
|
3763
|
+
*/
|
|
3764
|
+
export interface ValidationError {
|
|
3765
|
+
/** Error severity */
|
|
3766
|
+
severity: 'error' | 'warning' | 'information';
|
|
3767
|
+
/** Error code */
|
|
3768
|
+
code: string;
|
|
3769
|
+
/** Human-readable error message */
|
|
3770
|
+
message: string;
|
|
3771
|
+
/** Path to the invalid element */
|
|
3772
|
+
path: string;
|
|
3773
|
+
/** Current value that failed validation */
|
|
3774
|
+
value?: unknown;
|
|
3775
|
+
/** Expected value or constraint */
|
|
3776
|
+
expected?: string;
|
|
3777
|
+
/** Suggestion for fixing the error */
|
|
3778
|
+
suggestion?: string;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
/**
|
|
3782
|
+
* Validation warning details
|
|
3783
|
+
*/
|
|
3784
|
+
export interface ValidationWarning {
|
|
3785
|
+
/** Warning code */
|
|
3786
|
+
code: string;
|
|
3787
|
+
/** Human-readable warning message */
|
|
3788
|
+
message: string;
|
|
3789
|
+
/** Path to the element */
|
|
3790
|
+
path: string;
|
|
3791
|
+
/** Current value */
|
|
3792
|
+
value?: unknown;
|
|
3793
|
+
/** Suggestion for improvement */
|
|
3794
|
+
suggestion?: string;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
/**
|
|
3798
|
+
* Validation result
|
|
3799
|
+
*/
|
|
3800
|
+
export interface ValidationResult {
|
|
3801
|
+
/** Whether validation passed */
|
|
3802
|
+
valid: boolean;
|
|
3803
|
+
/** List of validation errors */
|
|
3804
|
+
errors: ValidationError[];
|
|
3805
|
+
/** List of validation warnings */
|
|
3806
|
+
warnings: ValidationWarning[];
|
|
3807
|
+
/** Validation performance metrics */
|
|
3808
|
+
metrics?: {
|
|
3809
|
+
/** Time taken for validation in milliseconds */
|
|
3810
|
+
duration: number;
|
|
3811
|
+
/** Number of elements validated */
|
|
3812
|
+
elementsValidated: number;
|
|
3813
|
+
/** Number of constraints checked */
|
|
3814
|
+
constraintsChecked: number;
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
/**
|
|
3819
|
+
* Validation exception thrown when validation fails and throwOnError is true
|
|
3820
|
+
*/
|
|
3821
|
+
export class ValidationException extends Error {
|
|
3822
|
+
public errors: ValidationError[];
|
|
3823
|
+
public warnings: ValidationWarning[];
|
|
3824
|
+
public result: ValidationResult;
|
|
3825
|
+
|
|
3826
|
+
constructor(result: ValidationResult) {
|
|
3827
|
+
const errorCount = result.errors.length;
|
|
3828
|
+
const warningCount = result.warnings.length;
|
|
3829
|
+
super(\`Validation failed: \${errorCount} error(s), \${warningCount} warning(s)\`);
|
|
3830
|
+
|
|
3831
|
+
this.name = 'ValidationException';
|
|
3832
|
+
this.errors = result.errors;
|
|
3833
|
+
this.warnings = result.warnings;
|
|
3834
|
+
this.result = result;
|
|
3835
|
+
}
|
|
3836
|
+
}`;
|
|
3837
|
+
}
|
|
3838
|
+
generateResourceValidators() {
|
|
3839
|
+
const resourceTypesArray = Array.from(this.resourceTypes).sort();
|
|
3840
|
+
return `/**
|
|
3841
|
+
* FHIR Resource Validators
|
|
3842
|
+
*
|
|
3843
|
+
* Client-side validation logic for FHIR resources.
|
|
3844
|
+
* Generated automatically from FHIR schemas.
|
|
3845
|
+
*/
|
|
3846
|
+
|
|
3847
|
+
import type { ResourceTypes, ResourceTypeMap, ${resourceTypesArray.join(", ")} } from './utility';
|
|
3848
|
+
import type { ValidationOptions, ValidationResult, ValidationError, ValidationWarning, ValidationException } from './validation-types';
|
|
3849
|
+
|
|
3850
|
+
/**
|
|
3851
|
+
* Main Resource Validator class
|
|
3852
|
+
*
|
|
3853
|
+
* Provides validation methods for all FHIR resource types with configurable
|
|
3854
|
+
* validation profiles and detailed error reporting.
|
|
3855
|
+
*/
|
|
3856
|
+
export class ResourceValidator {
|
|
3857
|
+
private static defaultOptions: Required<ValidationOptions> = {
|
|
3858
|
+
profile: 'strict',
|
|
3859
|
+
throwOnError: false,
|
|
3860
|
+
validateRequired: true,
|
|
3861
|
+
validateCardinality: true,
|
|
3862
|
+
validateTypes: true,
|
|
3863
|
+
validateConstraints: true,
|
|
3864
|
+
collectMetrics: false
|
|
3865
|
+
};
|
|
3866
|
+
|
|
3867
|
+
/**
|
|
3868
|
+
* Validate any FHIR resource with type safety
|
|
3869
|
+
*/
|
|
3870
|
+
static validate<T extends ResourceTypes>(
|
|
3871
|
+
resource: ResourceTypeMap[T],
|
|
3872
|
+
options: ValidationOptions = {}
|
|
3873
|
+
): ValidationResult {
|
|
3874
|
+
const opts = { ...this.defaultOptions, ...options };
|
|
3875
|
+
const startTime = opts.collectMetrics ? performance.now() : 0;
|
|
3876
|
+
|
|
3877
|
+
const result: ValidationResult = {
|
|
3878
|
+
valid: true,
|
|
3879
|
+
errors: [],
|
|
3880
|
+
warnings: []
|
|
3881
|
+
};
|
|
3882
|
+
|
|
3883
|
+
try {
|
|
3884
|
+
// Basic resource type validation
|
|
3885
|
+
if (!resource || typeof resource !== 'object') {
|
|
3886
|
+
result.errors.push({
|
|
3887
|
+
severity: 'error',
|
|
3888
|
+
code: 'INVALID_RESOURCE',
|
|
3889
|
+
message: 'Resource must be a valid object',
|
|
3890
|
+
path: 'resource',
|
|
3891
|
+
value: resource,
|
|
3892
|
+
expected: 'object',
|
|
3893
|
+
suggestion: 'Provide a valid FHIR resource object'
|
|
3894
|
+
});
|
|
3895
|
+
result.valid = false;
|
|
3896
|
+
} else {
|
|
3897
|
+
// Validate resource type
|
|
3898
|
+
const resourceType = resource.resourceType as T;
|
|
3899
|
+
if (!resourceType) {
|
|
3900
|
+
result.errors.push({
|
|
3901
|
+
severity: 'error',
|
|
3902
|
+
code: 'MISSING_RESOURCE_TYPE',
|
|
3903
|
+
message: 'Resource must have a resourceType property',
|
|
3904
|
+
path: 'resourceType',
|
|
3905
|
+
value: undefined,
|
|
3906
|
+
expected: 'string',
|
|
3907
|
+
suggestion: 'Add a resourceType property to the resource'
|
|
3908
|
+
});
|
|
3909
|
+
result.valid = false;
|
|
3910
|
+
} else {
|
|
3911
|
+
// Call resource-specific validator
|
|
3912
|
+
this.validateResourceType(resourceType, resource, result, opts);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
// Add performance metrics if requested
|
|
3917
|
+
if (opts.collectMetrics) {
|
|
3918
|
+
const endTime = performance.now();
|
|
3919
|
+
result.metrics = {
|
|
3920
|
+
duration: endTime - startTime,
|
|
3921
|
+
elementsValidated: this.countElements(resource),
|
|
3922
|
+
constraintsChecked: result.errors.length + result.warnings.length
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// Throw exception if requested and validation failed
|
|
3927
|
+
if (opts.throwOnError && !result.valid) {
|
|
3928
|
+
const { ValidationException } = require('./validation-types');
|
|
3929
|
+
throw new ValidationException(result);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
return result;
|
|
3933
|
+
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (error instanceof Error && error.name === 'ValidationException') {
|
|
3936
|
+
throw error;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
result.errors.push({
|
|
3940
|
+
severity: 'error',
|
|
3941
|
+
code: 'VALIDATION_ERROR',
|
|
3942
|
+
message: \`Validation failed: \${error instanceof Error ? error.message : String(error)}\`,
|
|
3943
|
+
path: 'resource',
|
|
3944
|
+
value: resource,
|
|
3945
|
+
suggestion: 'Check the resource structure and try again'
|
|
3946
|
+
});
|
|
3947
|
+
result.valid = false;
|
|
3948
|
+
|
|
3949
|
+
if (opts.throwOnError) {
|
|
3950
|
+
const { ValidationException } = require('./validation-types');
|
|
3951
|
+
throw new ValidationException(result);
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
return result;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
${this.generateResourceSpecificValidators()}
|
|
3959
|
+
|
|
3960
|
+
/**
|
|
3961
|
+
* Validate resource type and dispatch to specific validator
|
|
3962
|
+
*/
|
|
3963
|
+
private static validateResourceType<T extends ResourceTypes>(
|
|
3964
|
+
resourceType: T,
|
|
3965
|
+
resource: ResourceTypeMap[T],
|
|
3966
|
+
result: ValidationResult,
|
|
3967
|
+
options: Required<ValidationOptions>
|
|
3968
|
+
): void {
|
|
3969
|
+
switch (resourceType) {
|
|
3970
|
+
${resourceTypesArray.map((type) => ` case '${type}':
|
|
3971
|
+
this.validate${type}(resource as ${type}, result, options);
|
|
3972
|
+
break;`).join(`
|
|
3973
|
+
`)}
|
|
3974
|
+
default:
|
|
3975
|
+
result.warnings.push({
|
|
3976
|
+
code: 'UNKNOWN_RESOURCE_TYPE',
|
|
3977
|
+
message: \`Unknown resource type: \${resourceType}\`,
|
|
3978
|
+
path: 'resourceType',
|
|
3979
|
+
value: resourceType,
|
|
3980
|
+
suggestion: 'Check if the resource type is supported'
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
/**
|
|
3986
|
+
* Count elements in resource for metrics
|
|
3987
|
+
*/
|
|
3988
|
+
private static countElements(resource: any, count = 0): number {
|
|
3989
|
+
if (!resource || typeof resource !== 'object') return count;
|
|
3990
|
+
|
|
3991
|
+
for (const value of Object.values(resource)) {
|
|
3992
|
+
count++;
|
|
3993
|
+
if (Array.isArray(value)) {
|
|
3994
|
+
for (const item of value) {
|
|
3995
|
+
count = this.countElements(item, count);
|
|
3996
|
+
}
|
|
3997
|
+
} else if (typeof value === 'object') {
|
|
3998
|
+
count = this.countElements(value, count);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
return count;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
/**
|
|
4006
|
+
* Validate required fields
|
|
4007
|
+
*/
|
|
4008
|
+
private static validateRequired(
|
|
4009
|
+
resource: any,
|
|
4010
|
+
requiredFields: string[],
|
|
4011
|
+
result: ValidationResult,
|
|
4012
|
+
basePath = ''
|
|
4013
|
+
): void {
|
|
4014
|
+
for (const field of requiredFields) {
|
|
4015
|
+
const path = basePath ? \`\${basePath}.\${field}\` : field;
|
|
4016
|
+
if (resource[field] === undefined || resource[field] === null) {
|
|
4017
|
+
result.errors.push({
|
|
4018
|
+
severity: 'error',
|
|
4019
|
+
code: 'MISSING_REQUIRED_FIELD',
|
|
4020
|
+
message: \`Required field '\${field}' is missing\`,
|
|
4021
|
+
path,
|
|
4022
|
+
value: resource[field],
|
|
4023
|
+
expected: 'non-null value',
|
|
4024
|
+
suggestion: \`Add the required '\${field}' field to the resource\`
|
|
4025
|
+
});
|
|
4026
|
+
result.valid = false;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
/**
|
|
4032
|
+
* Validate field type
|
|
4033
|
+
*/
|
|
4034
|
+
private static validateFieldType(
|
|
4035
|
+
value: any,
|
|
4036
|
+
expectedType: string,
|
|
4037
|
+
fieldName: string,
|
|
4038
|
+
result: ValidationResult,
|
|
4039
|
+
basePath = ''
|
|
4040
|
+
): void {
|
|
4041
|
+
const path = basePath ? \`\${basePath}.\${fieldName}\` : fieldName;
|
|
4042
|
+
|
|
4043
|
+
if (value === undefined || value === null) return;
|
|
4044
|
+
|
|
4045
|
+
let isValid = false;
|
|
4046
|
+
switch (expectedType) {
|
|
4047
|
+
case 'string':
|
|
4048
|
+
isValid = typeof value === 'string';
|
|
4049
|
+
break;
|
|
4050
|
+
case 'number':
|
|
4051
|
+
isValid = typeof value === 'number' && !isNaN(value);
|
|
4052
|
+
break;
|
|
4053
|
+
case 'boolean':
|
|
4054
|
+
isValid = typeof value === 'boolean';
|
|
4055
|
+
break;
|
|
4056
|
+
case 'array':
|
|
4057
|
+
isValid = Array.isArray(value);
|
|
4058
|
+
break;
|
|
4059
|
+
case 'object':
|
|
4060
|
+
isValid = typeof value === 'object' && !Array.isArray(value);
|
|
4061
|
+
break;
|
|
4062
|
+
default:
|
|
4063
|
+
isValid = true; // Skip unknown types
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
if (!isValid) {
|
|
4067
|
+
result.errors.push({
|
|
4068
|
+
severity: 'error',
|
|
4069
|
+
code: 'INVALID_FIELD_TYPE',
|
|
4070
|
+
message: \`Field '\${fieldName}' has invalid type\`,
|
|
4071
|
+
path,
|
|
4072
|
+
value,
|
|
4073
|
+
expected: expectedType,
|
|
4074
|
+
suggestion: \`Ensure '\${fieldName}' is of type \${expectedType}\`
|
|
4075
|
+
});
|
|
4076
|
+
result.valid = false;
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
}`;
|
|
4080
|
+
}
|
|
4081
|
+
generateResourceSpecificValidators() {
|
|
4082
|
+
const validators = [];
|
|
4083
|
+
const keyResourceTypes = [
|
|
4084
|
+
"Patient",
|
|
4085
|
+
"Observation",
|
|
4086
|
+
"Organization",
|
|
4087
|
+
"Practitioner",
|
|
4088
|
+
"Bundle"
|
|
4089
|
+
];
|
|
4090
|
+
for (const resourceType of keyResourceTypes) {
|
|
4091
|
+
if (this.resourceTypes.has(resourceType)) {
|
|
4092
|
+
validators.push(this.generateResourceValidator(resourceType));
|
|
3127
4093
|
}
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
if (!subType.isPrimitive && !nestedOpts?.isNested) {
|
|
3133
|
-
this.imports.set(subType.value, subType.value);
|
|
4094
|
+
}
|
|
4095
|
+
for (const resourceType of this.resourceTypes) {
|
|
4096
|
+
if (!keyResourceTypes.includes(resourceType)) {
|
|
4097
|
+
validators.push(this.generateGenericResourceValidator(resourceType));
|
|
3134
4098
|
}
|
|
3135
|
-
typeString = subType.value;
|
|
3136
4099
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
4100
|
+
return validators.join(`
|
|
4101
|
+
|
|
4102
|
+
`);
|
|
4103
|
+
}
|
|
4104
|
+
generateResourceValidator(resourceType) {
|
|
4105
|
+
const validationRules = this.getValidationRules(resourceType);
|
|
4106
|
+
return ` /**
|
|
4107
|
+
* Validate ${resourceType} resource
|
|
4108
|
+
*/
|
|
4109
|
+
private static validate${resourceType}(
|
|
4110
|
+
resource: ${resourceType},
|
|
4111
|
+
result: ValidationResult,
|
|
4112
|
+
options: Required<ValidationOptions>
|
|
4113
|
+
): void {
|
|
4114
|
+
// Validate required fields
|
|
4115
|
+
if (options.validateRequired) {
|
|
4116
|
+
this.validateRequired(resource, [${validationRules.required.map((f) => `'${f}'`).join(", ")}], result, '${resourceType.toLowerCase()}');
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// Validate field types
|
|
4120
|
+
if (options.validateTypes) {
|
|
4121
|
+
${validationRules.fields.map((field) => `if (resource.${field.name} !== undefined) {
|
|
4122
|
+
this.validateFieldType(resource.${field.name}, '${field.type}', '${field.name}', result, '${resourceType.toLowerCase()}');
|
|
4123
|
+
}`).join(`
|
|
4124
|
+
`)}
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// Validate specific constraints for ${resourceType}
|
|
4128
|
+
if (options.validateConstraints) {
|
|
4129
|
+
${this.generateResourceSpecificConstraints(resourceType)}
|
|
4130
|
+
}
|
|
4131
|
+
}`;
|
|
4132
|
+
}
|
|
4133
|
+
generateGenericResourceValidator(resourceType) {
|
|
4134
|
+
return ` /**
|
|
4135
|
+
* Validate ${resourceType} resource (generic validation)
|
|
4136
|
+
*/
|
|
4137
|
+
private static validate${resourceType}(
|
|
4138
|
+
resource: ${resourceType},
|
|
4139
|
+
result: ValidationResult,
|
|
4140
|
+
options: Required<ValidationOptions>
|
|
4141
|
+
): void {
|
|
4142
|
+
// Basic validation for ${resourceType}
|
|
4143
|
+
if (options.validateRequired && resource.resourceType !== '${resourceType}') {
|
|
4144
|
+
result.errors.push({
|
|
4145
|
+
severity: 'error',
|
|
4146
|
+
code: 'INVALID_RESOURCE_TYPE',
|
|
4147
|
+
message: \`Expected resourceType '${resourceType}', got '\${resource.resourceType}'\`,
|
|
4148
|
+
path: 'resourceType',
|
|
4149
|
+
value: resource.resourceType,
|
|
4150
|
+
expected: '${resourceType}',
|
|
4151
|
+
suggestion: 'Ensure the resourceType matches the expected value'
|
|
4152
|
+
});
|
|
4153
|
+
result.valid = false;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
// Generic field validation
|
|
4157
|
+
if (options.validateTypes) {
|
|
4158
|
+
// Validate common fields
|
|
4159
|
+
if (resource.id !== undefined) {
|
|
4160
|
+
this.validateFieldType(resource.id, 'string', 'id', result, '${resourceType.toLowerCase()}');
|
|
4161
|
+
}
|
|
4162
|
+
if ((resource as any).meta !== undefined) {
|
|
4163
|
+
this.validateFieldType((resource as any).meta, 'object', 'meta', result, '${resourceType.toLowerCase()}');
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
}`;
|
|
4167
|
+
}
|
|
4168
|
+
getValidationRules(resourceType) {
|
|
4169
|
+
switch (resourceType) {
|
|
4170
|
+
case "Patient":
|
|
4171
|
+
return {
|
|
4172
|
+
required: ["resourceType"],
|
|
4173
|
+
fields: [
|
|
4174
|
+
{ name: "id", type: "string" },
|
|
4175
|
+
{ name: "meta", type: "object" },
|
|
4176
|
+
{ name: "identifier", type: "array" },
|
|
4177
|
+
{ name: "active", type: "boolean" },
|
|
4178
|
+
{ name: "name", type: "array" },
|
|
4179
|
+
{ name: "telecom", type: "array" },
|
|
4180
|
+
{ name: "gender", type: "string" },
|
|
4181
|
+
{ name: "birthDate", type: "string" },
|
|
4182
|
+
{ name: "address", type: "array" }
|
|
4183
|
+
]
|
|
4184
|
+
};
|
|
4185
|
+
case "Observation":
|
|
4186
|
+
return {
|
|
4187
|
+
required: ["resourceType", "status", "code"],
|
|
4188
|
+
fields: [
|
|
4189
|
+
{ name: "id", type: "string" },
|
|
4190
|
+
{ name: "meta", type: "object" },
|
|
4191
|
+
{ name: "identifier", type: "array" },
|
|
4192
|
+
{ name: "status", type: "string" },
|
|
4193
|
+
{ name: "category", type: "array" },
|
|
4194
|
+
{ name: "code", type: "object" },
|
|
4195
|
+
{ name: "subject", type: "object" },
|
|
4196
|
+
{ name: "effectiveDateTime", type: "string" },
|
|
4197
|
+
{ name: "valueQuantity", type: "object" },
|
|
4198
|
+
{ name: "valueString", type: "string" },
|
|
4199
|
+
{ name: "valueBoolean", type: "boolean" }
|
|
4200
|
+
]
|
|
4201
|
+
};
|
|
4202
|
+
case "Organization":
|
|
4203
|
+
return {
|
|
4204
|
+
required: ["resourceType"],
|
|
4205
|
+
fields: [
|
|
4206
|
+
{ name: "id", type: "string" },
|
|
4207
|
+
{ name: "meta", type: "object" },
|
|
4208
|
+
{ name: "identifier", type: "array" },
|
|
4209
|
+
{ name: "active", type: "boolean" },
|
|
4210
|
+
{ name: "type", type: "array" },
|
|
4211
|
+
{ name: "name", type: "string" },
|
|
4212
|
+
{ name: "telecom", type: "array" },
|
|
4213
|
+
{ name: "address", type: "array" }
|
|
4214
|
+
]
|
|
4215
|
+
};
|
|
4216
|
+
case "Practitioner":
|
|
4217
|
+
return {
|
|
4218
|
+
required: ["resourceType"],
|
|
4219
|
+
fields: [
|
|
4220
|
+
{ name: "id", type: "string" },
|
|
4221
|
+
{ name: "meta", type: "object" },
|
|
4222
|
+
{ name: "identifier", type: "array" },
|
|
4223
|
+
{ name: "active", type: "boolean" },
|
|
4224
|
+
{ name: "name", type: "array" },
|
|
4225
|
+
{ name: "telecom", type: "array" },
|
|
4226
|
+
{ name: "address", type: "array" },
|
|
4227
|
+
{ name: "gender", type: "string" },
|
|
4228
|
+
{ name: "birthDate", type: "string" }
|
|
4229
|
+
]
|
|
4230
|
+
};
|
|
4231
|
+
case "Bundle":
|
|
4232
|
+
return {
|
|
4233
|
+
required: ["resourceType", "type"],
|
|
4234
|
+
fields: [
|
|
4235
|
+
{ name: "id", type: "string" },
|
|
4236
|
+
{ name: "meta", type: "object" },
|
|
4237
|
+
{ name: "identifier", type: "object" },
|
|
4238
|
+
{ name: "type", type: "string" },
|
|
4239
|
+
{ name: "timestamp", type: "string" },
|
|
4240
|
+
{ name: "total", type: "number" },
|
|
4241
|
+
{ name: "entry", type: "array" }
|
|
4242
|
+
]
|
|
4243
|
+
};
|
|
4244
|
+
default:
|
|
4245
|
+
return {
|
|
4246
|
+
required: ["resourceType"],
|
|
4247
|
+
fields: [
|
|
4248
|
+
{ name: "id", type: "string" },
|
|
4249
|
+
{ name: "meta", type: "object" }
|
|
4250
|
+
]
|
|
4251
|
+
};
|
|
3140
4252
|
}
|
|
3141
|
-
const optional = !field.required ? "?" : "";
|
|
3142
|
-
const array = field.array ? "[]" : "";
|
|
3143
|
-
return `${fieldName}${optional}: ${nestedOpts?.isNested && nestedOpts.baseName ? nestedOpts.baseName : ""}${typeString}${array};`;
|
|
3144
4253
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
4254
|
+
generateResourceSpecificConstraints(resourceType) {
|
|
4255
|
+
switch (resourceType) {
|
|
4256
|
+
case "Patient":
|
|
4257
|
+
return `// Patient-specific constraints
|
|
4258
|
+
if (resource.gender && !['male', 'female', 'other', 'unknown'].includes(resource.gender)) {
|
|
4259
|
+
result.errors.push({
|
|
4260
|
+
severity: 'error',
|
|
4261
|
+
code: 'INVALID_GENDER_VALUE',
|
|
4262
|
+
message: 'Invalid gender value',
|
|
4263
|
+
path: 'patient.gender',
|
|
4264
|
+
value: resource.gender,
|
|
4265
|
+
expected: 'male, female, other, or unknown',
|
|
4266
|
+
suggestion: 'Use a valid gender code'
|
|
4267
|
+
});
|
|
4268
|
+
result.valid = false;
|
|
4269
|
+
}`;
|
|
4270
|
+
case "Observation":
|
|
4271
|
+
return `// Observation-specific constraints
|
|
4272
|
+
if (resource.status && !['registered', 'preliminary', 'final', 'amended', 'corrected', 'cancelled', 'entered-in-error', 'unknown'].includes(resource.status)) {
|
|
4273
|
+
result.errors.push({
|
|
4274
|
+
severity: 'error',
|
|
4275
|
+
code: 'INVALID_OBSERVATION_STATUS',
|
|
4276
|
+
message: 'Invalid observation status',
|
|
4277
|
+
path: 'observation.status',
|
|
4278
|
+
value: resource.status,
|
|
4279
|
+
expected: 'valid observation status code',
|
|
4280
|
+
suggestion: 'Use a valid observation status code'
|
|
4281
|
+
});
|
|
4282
|
+
result.valid = false;
|
|
4283
|
+
}`;
|
|
4284
|
+
case "Bundle":
|
|
4285
|
+
return `// Bundle-specific constraints
|
|
4286
|
+
if (resource.type && !['document', 'message', 'transaction', 'transaction-response', 'batch', 'batch-response', 'history', 'searchset', 'collection'].includes(resource.type)) {
|
|
4287
|
+
result.errors.push({
|
|
4288
|
+
severity: 'error',
|
|
4289
|
+
code: 'INVALID_BUNDLE_TYPE',
|
|
4290
|
+
message: 'Invalid bundle type',
|
|
4291
|
+
path: 'bundle.type',
|
|
4292
|
+
value: resource.type,
|
|
4293
|
+
expected: 'valid bundle type code',
|
|
4294
|
+
suggestion: 'Use a valid bundle type code'
|
|
4295
|
+
});
|
|
4296
|
+
result.valid = false;
|
|
4297
|
+
}`;
|
|
4298
|
+
default:
|
|
4299
|
+
return `// Generic resource constraints
|
|
4300
|
+
// No specific constraints for ${resourceType}`;
|
|
3149
4301
|
}
|
|
3150
|
-
|
|
4302
|
+
}
|
|
4303
|
+
getResourceTypes() {
|
|
4304
|
+
return this.resourceTypes;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
// src/api/generators/rest-client.ts
|
|
4309
|
+
class RestClientGenerator {
|
|
4310
|
+
options;
|
|
4311
|
+
resourceTypes = new Set;
|
|
4312
|
+
searchParameterEnhancer;
|
|
4313
|
+
validationGenerator;
|
|
4314
|
+
constructor(options) {
|
|
4315
|
+
this.options = {
|
|
4316
|
+
clientName: "FHIRClient",
|
|
4317
|
+
includeValidation: false,
|
|
4318
|
+
includeErrorHandling: true,
|
|
4319
|
+
includeRequestInterceptors: false,
|
|
4320
|
+
baseUrlOverride: "",
|
|
4321
|
+
enhancedSearch: false,
|
|
4322
|
+
includeUtilities: true,
|
|
4323
|
+
generateValidators: false,
|
|
4324
|
+
useCanonicalManager: true,
|
|
4325
|
+
defaultTimeout: 30000,
|
|
4326
|
+
defaultRetries: 0,
|
|
4327
|
+
includeDocumentation: true,
|
|
4328
|
+
generateExamples: false,
|
|
4329
|
+
chainedSearchBuilder: false,
|
|
4330
|
+
searchAutocomplete: true,
|
|
4331
|
+
generateValueSetEnums: true,
|
|
4332
|
+
...options
|
|
4333
|
+
};
|
|
4334
|
+
console.log(`[DEBUG] REST client configured with options:`, this.options);
|
|
4335
|
+
this.searchParameterEnhancer = new SearchParameterEnhancer({
|
|
4336
|
+
autocomplete: this.options.searchAutocomplete ?? false,
|
|
4337
|
+
valueSetEnums: this.options.generateValueSetEnums ?? false
|
|
4338
|
+
});
|
|
4339
|
+
this.validationGenerator = new ValidationGenerator;
|
|
4340
|
+
}
|
|
4341
|
+
collectResourceTypes(schemas) {
|
|
4342
|
+
this.resourceTypes.clear();
|
|
4343
|
+
for (const schema of schemas) {
|
|
4344
|
+
if (schema.identifier.kind === "resource" && schema.identifier.name !== "DomainResource" && schema.identifier.name !== "Resource") {
|
|
4345
|
+
this.resourceTypes.add(schema.identifier.name);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
if (this.options.enhancedSearch) {
|
|
4349
|
+
this.searchParameterEnhancer.collectResourceData(schemas);
|
|
4350
|
+
}
|
|
4351
|
+
if (this.options.includeValidation || this.options.generateValidators) {
|
|
4352
|
+
this.validationGenerator.collectResourceData(schemas);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
async generate(schemas) {
|
|
4356
|
+
this.collectResourceTypes(schemas);
|
|
4357
|
+
await mkdir2(this.options.outputDir, { recursive: true });
|
|
4358
|
+
const generatedFiles = [];
|
|
4359
|
+
const clientFile = await this.generateClientFile();
|
|
4360
|
+
const clientPath = join10(this.options.outputDir, clientFile.filename);
|
|
4361
|
+
await this.ensureDirectoryExists(clientPath);
|
|
4362
|
+
await writeFile4(clientPath, clientFile.content, "utf-8");
|
|
4363
|
+
generatedFiles.push({
|
|
4364
|
+
...clientFile,
|
|
4365
|
+
path: clientPath
|
|
4366
|
+
});
|
|
4367
|
+
const typesFile = await this.generateTypesFile();
|
|
4368
|
+
const typesPath = join10(this.options.outputDir, typesFile.filename);
|
|
4369
|
+
await writeFile4(typesPath, typesFile.content, "utf-8");
|
|
4370
|
+
generatedFiles.push({
|
|
4371
|
+
...typesFile,
|
|
4372
|
+
path: typesPath
|
|
4373
|
+
});
|
|
4374
|
+
if (this.options.enhancedSearch) {
|
|
4375
|
+
const searchParamsFile = await this.generateEnhancedSearchParamsFile();
|
|
4376
|
+
const searchParamsPath = join10(this.options.outputDir, searchParamsFile.filename);
|
|
4377
|
+
await writeFile4(searchParamsPath, searchParamsFile.content, "utf-8");
|
|
4378
|
+
generatedFiles.push({
|
|
4379
|
+
...searchParamsFile,
|
|
4380
|
+
path: searchParamsPath
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
if (this.options.includeValidation || this.options.generateValidators) {
|
|
4384
|
+
const validationTypesFile = await this.generateValidationTypesFile();
|
|
4385
|
+
const validationTypesPath = join10(this.options.outputDir, validationTypesFile.filename);
|
|
4386
|
+
await writeFile4(validationTypesPath, validationTypesFile.content, "utf-8");
|
|
4387
|
+
generatedFiles.push({
|
|
4388
|
+
...validationTypesFile,
|
|
4389
|
+
path: validationTypesPath
|
|
4390
|
+
});
|
|
4391
|
+
const validatorsFile = await this.generateValidatorsFile();
|
|
4392
|
+
const validatorsPath = join10(this.options.outputDir, validatorsFile.filename);
|
|
4393
|
+
await writeFile4(validatorsPath, validatorsFile.content, "utf-8");
|
|
4394
|
+
generatedFiles.push({
|
|
4395
|
+
...validatorsFile,
|
|
4396
|
+
path: validatorsPath
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
const utilityFile = await this.generateUtilityFile();
|
|
4400
|
+
const utilityPath = join10(this.options.outputDir, utilityFile.filename);
|
|
4401
|
+
await writeFile4(utilityPath, utilityFile.content, "utf-8");
|
|
4402
|
+
generatedFiles.push({
|
|
4403
|
+
...utilityFile,
|
|
4404
|
+
path: utilityPath
|
|
4405
|
+
});
|
|
4406
|
+
return generatedFiles;
|
|
4407
|
+
}
|
|
4408
|
+
async generateClientFile() {
|
|
4409
|
+
const resourceTypesArray = Array.from(this.resourceTypes).sort();
|
|
4410
|
+
const clientName = this.options.clientName;
|
|
4411
|
+
const enhancedSearchImports = this.options.enhancedSearch ? `import type {
|
|
4412
|
+
EnhancedSearchParams,
|
|
4413
|
+
SearchParameterValidator${this.options.searchAutocomplete ? `,
|
|
4414
|
+
SearchParamName,
|
|
4415
|
+
BaseEnhancedSearchParams` : ""}
|
|
4416
|
+
} from './enhanced-search-params';` : "";
|
|
4417
|
+
const validationImports = this.options.includeValidation || this.options.generateValidators ? `import type {
|
|
4418
|
+
ValidationOptions,
|
|
4419
|
+
ValidationResult,
|
|
4420
|
+
ValidationException
|
|
4421
|
+
} from './validation-types';
|
|
4422
|
+
import { ResourceValidator } from './resource-validators';` : "";
|
|
4423
|
+
const searchParamType = "SearchParams,";
|
|
4424
|
+
const content = `/**
|
|
4425
|
+
* FHIR REST Client
|
|
4426
|
+
*
|
|
4427
|
+
* Type-safe FHIR REST client with autocompletion for all resource types.
|
|
4428
|
+
* Generated automatically from FHIR schemas.
|
|
4429
|
+
*/
|
|
4430
|
+
|
|
4431
|
+
import type {
|
|
4432
|
+
ResourceTypes,
|
|
4433
|
+
Bundle,
|
|
4434
|
+
OperationOutcome,
|
|
4435
|
+
${resourceTypesArray.join(`,
|
|
4436
|
+
`)}
|
|
4437
|
+
} from '../types';
|
|
4438
|
+
import type {
|
|
4439
|
+
${clientName}Config,
|
|
4440
|
+
${searchParamType}
|
|
4441
|
+
CreateResponse,
|
|
4442
|
+
UpdateResponse,
|
|
4443
|
+
DeleteResponse,
|
|
4444
|
+
ReadResponse,
|
|
4445
|
+
SearchResponse,
|
|
4446
|
+
RequestOptions,
|
|
4447
|
+
HTTPMethod
|
|
4448
|
+
} from './client-types';
|
|
4449
|
+
${enhancedSearchImports}
|
|
4450
|
+
${validationImports}
|
|
4451
|
+
import type { ResourceTypeMap } from './utility';
|
|
4452
|
+
|
|
4453
|
+
/**
|
|
4454
|
+
* Main FHIR REST Client
|
|
4455
|
+
*
|
|
4456
|
+
* Provides type-safe operations for all FHIR resources with autocompletion.
|
|
4457
|
+
*/
|
|
4458
|
+
export class ${clientName} {
|
|
4459
|
+
private baseUrl: string;
|
|
4460
|
+
private config: Required<${clientName}Config>;
|
|
4461
|
+
|
|
4462
|
+
constructor(baseUrl: string, config: ${clientName}Config = {}) {
|
|
4463
|
+
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
4464
|
+
this.config = {
|
|
4465
|
+
timeout: 30000,
|
|
4466
|
+
retries: 0,
|
|
4467
|
+
headers: {},
|
|
4468
|
+
validateResponses: false,
|
|
4469
|
+
...config
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
${this.generateCRUDMethods()}
|
|
4474
|
+
|
|
4475
|
+
${this.generateSearchMethod()}
|
|
4476
|
+
|
|
4477
|
+
/**
|
|
4478
|
+
* Get server capability statement
|
|
4479
|
+
*/
|
|
4480
|
+
async getCapabilities(): Promise<any> {
|
|
4481
|
+
const url = \`\${this.baseUrl}/metadata\`;
|
|
4482
|
+
return this.request('GET', url);
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
/**
|
|
4486
|
+
* Execute raw HTTP request with full control
|
|
4487
|
+
*/
|
|
4488
|
+
async request<T = any>(
|
|
4489
|
+
method: HTTPMethod,
|
|
4490
|
+
url: string,
|
|
4491
|
+
body?: any,
|
|
4492
|
+
options?: RequestOptions
|
|
4493
|
+
): Promise<T> {
|
|
4494
|
+
const requestOptions: RequestInit = {
|
|
4495
|
+
method,
|
|
4496
|
+
headers: {
|
|
4497
|
+
'Content-Type': 'application/fhir+json',
|
|
4498
|
+
'Accept': 'application/fhir+json',
|
|
4499
|
+
...this.config.headers,
|
|
4500
|
+
...options?.headers
|
|
4501
|
+
},
|
|
4502
|
+
signal: options?.signal || (this.config.timeout > 0
|
|
4503
|
+
? AbortSignal.timeout(this.config.timeout)
|
|
4504
|
+
: undefined
|
|
4505
|
+
)
|
|
4506
|
+
};
|
|
4507
|
+
|
|
4508
|
+
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
4509
|
+
requestOptions.body = JSON.stringify(body);
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
${this.options.includeErrorHandling ? this.generateErrorHandling() : "const response = await fetch(url, requestOptions);"}
|
|
4513
|
+
|
|
4514
|
+
if (!response.ok) {
|
|
4515
|
+
await this.handleErrorResponse(response);
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
// Handle different response types
|
|
4519
|
+
const contentType = response.headers.get('content-type');
|
|
4520
|
+
if (contentType?.includes('application/json') || contentType?.includes('application/fhir+json')) {
|
|
4521
|
+
return response.json();
|
|
4522
|
+
} else if (method === 'DELETE') {
|
|
4523
|
+
return undefined as T;
|
|
4524
|
+
} else {
|
|
4525
|
+
return response.text() as T;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
${this.options.includeErrorHandling ? this.generateErrorHandlingMethods() : ""}
|
|
4530
|
+
|
|
4531
|
+
/**
|
|
4532
|
+
* Update client configuration
|
|
4533
|
+
*/
|
|
4534
|
+
updateConfig(config: Partial<${clientName}Config>): void {
|
|
4535
|
+
this.config = { ...this.config, ...config };
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
/**
|
|
4539
|
+
* Get current configuration
|
|
4540
|
+
*/
|
|
4541
|
+
getConfig(): Required<${clientName}Config> {
|
|
4542
|
+
return { ...this.config };
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
/**
|
|
4546
|
+
* Get base URL
|
|
4547
|
+
*/
|
|
4548
|
+
getBaseUrl(): string {
|
|
4549
|
+
return this.baseUrl;
|
|
4550
|
+
}${this.generateValidationMethods()}
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
export default ${clientName};`;
|
|
4554
|
+
return {
|
|
4555
|
+
filename: `${clientName.toLowerCase()}.ts`,
|
|
4556
|
+
content,
|
|
4557
|
+
exports: [clientName]
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4560
|
+
async generateTypesFile() {
|
|
4561
|
+
const content = `/**
|
|
4562
|
+
* FHIR REST Client Types
|
|
4563
|
+
*
|
|
4564
|
+
* Type definitions for the FHIR REST client.
|
|
4565
|
+
*/
|
|
4566
|
+
|
|
4567
|
+
import type { Bundle } from '../types';
|
|
4568
|
+
import type { ResourceTypeMap } from './utility';
|
|
4569
|
+
|
|
4570
|
+
/**
|
|
4571
|
+
* HTTP methods supported by the client
|
|
4572
|
+
*/
|
|
4573
|
+
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
4574
|
+
|
|
4575
|
+
/**
|
|
4576
|
+
* Client configuration options
|
|
4577
|
+
*/
|
|
4578
|
+
export interface ${this.options.clientName}Config {
|
|
4579
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
4580
|
+
timeout?: number;
|
|
4581
|
+
/** Number of retries for failed requests (default: 0) */
|
|
4582
|
+
retries?: number;
|
|
4583
|
+
/** Default headers to include with all requests */
|
|
4584
|
+
headers?: Record<string, string>;
|
|
4585
|
+
/** Whether to validate response schemas (default: false) */
|
|
4586
|
+
validateResponses?: boolean;${this.generateValidationConfigFields()}
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
/**
|
|
4590
|
+
* Request options for individual operations
|
|
4591
|
+
*/
|
|
4592
|
+
export interface RequestOptions {
|
|
4593
|
+
/** Additional headers for this request */
|
|
4594
|
+
headers?: Record<string, string>;
|
|
4595
|
+
/** AbortSignal to cancel the request */
|
|
4596
|
+
signal?: AbortSignal;
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
/**
|
|
4600
|
+
* Generic search parameters
|
|
4601
|
+
*/
|
|
4602
|
+
export interface SearchParams {
|
|
4603
|
+
/** Number of results to return */
|
|
4604
|
+
_count?: number;
|
|
4605
|
+
/** Pagination offset */
|
|
4606
|
+
_offset?: number;
|
|
4607
|
+
/** Include related resources */
|
|
4608
|
+
_include?: string | string[];
|
|
4609
|
+
/** Reverse include */
|
|
4610
|
+
_revinclude?: string | string[];
|
|
4611
|
+
/** Summary mode */
|
|
4612
|
+
_summary?: 'true' | 'false' | 'text' | 'data' | 'count';
|
|
4613
|
+
/** Elements to include */
|
|
4614
|
+
_elements?: string | string[];
|
|
4615
|
+
/** Any other FHIR search parameters */
|
|
4616
|
+
[key: string]: any;
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
/**
|
|
4620
|
+
* Response type for create operations
|
|
4621
|
+
*/
|
|
4622
|
+
export interface CreateResponse<T extends keyof ResourceTypeMap> {
|
|
4623
|
+
/** The created resource */
|
|
4624
|
+
resource: ResourceTypeMap[T];
|
|
4625
|
+
/** Response status code */
|
|
4626
|
+
status: number;
|
|
4627
|
+
/** Response headers */
|
|
4628
|
+
headers: Headers;
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
/**
|
|
4632
|
+
* Response type for read operations
|
|
4633
|
+
*/
|
|
4634
|
+
export interface ReadResponse<T extends keyof ResourceTypeMap> {
|
|
4635
|
+
/** The retrieved resource */
|
|
4636
|
+
resource: ResourceTypeMap[T];
|
|
4637
|
+
/** Response status code */
|
|
4638
|
+
status: number;
|
|
4639
|
+
/** Response headers */
|
|
4640
|
+
headers: Headers;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
/**
|
|
4644
|
+
* Response type for update operations
|
|
4645
|
+
*/
|
|
4646
|
+
export interface UpdateResponse<T extends keyof ResourceTypeMap> {
|
|
4647
|
+
/** The updated resource */
|
|
4648
|
+
resource: ResourceTypeMap[T];
|
|
4649
|
+
/** Response status code */
|
|
4650
|
+
status: number;
|
|
4651
|
+
/** Response headers */
|
|
4652
|
+
headers: Headers;
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
/**
|
|
4656
|
+
* Response type for delete operations
|
|
4657
|
+
*/
|
|
4658
|
+
export interface DeleteResponse {
|
|
4659
|
+
/** Response status code */
|
|
4660
|
+
status: number;
|
|
4661
|
+
/** Response headers */
|
|
4662
|
+
headers: Headers;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
/**
|
|
4666
|
+
* Response type for search operations
|
|
4667
|
+
*/
|
|
4668
|
+
export interface SearchResponse<T extends keyof ResourceTypeMap> {
|
|
4669
|
+
/** The search result bundle */
|
|
4670
|
+
bundle: Bundle<ResourceTypeMap[T]>;
|
|
4671
|
+
/** Response status code */
|
|
4672
|
+
status: number;
|
|
4673
|
+
/** Response headers */
|
|
4674
|
+
headers: Headers;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
/**
|
|
4678
|
+
* FHIR operation outcome for errors
|
|
4679
|
+
*/
|
|
4680
|
+
export interface FHIRError extends Error {
|
|
4681
|
+
/** FHIR OperationOutcome */
|
|
4682
|
+
operationOutcome?: import('../types').OperationOutcome;
|
|
4683
|
+
/** HTTP status code */
|
|
4684
|
+
status?: number;
|
|
4685
|
+
/** Response headers */
|
|
4686
|
+
headers?: Headers;
|
|
4687
|
+
}`;
|
|
4688
|
+
return {
|
|
4689
|
+
filename: "client-types.ts",
|
|
4690
|
+
content,
|
|
4691
|
+
exports: [
|
|
4692
|
+
"HTTPMethod",
|
|
4693
|
+
`${this.options.clientName}Config`,
|
|
4694
|
+
"RequestOptions",
|
|
4695
|
+
"SearchParams",
|
|
4696
|
+
"CreateResponse",
|
|
4697
|
+
"ReadResponse",
|
|
4698
|
+
"UpdateResponse",
|
|
4699
|
+
"DeleteResponse",
|
|
4700
|
+
"SearchResponse",
|
|
4701
|
+
"FHIRError"
|
|
4702
|
+
]
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
generateErrorHandling() {
|
|
4706
|
+
return `let response: Response;
|
|
4707
|
+
let retryCount = 0;
|
|
4708
|
+
|
|
4709
|
+
while (retryCount <= this.config.retries) {
|
|
4710
|
+
try {
|
|
4711
|
+
response = await fetch(url, requestOptions);
|
|
4712
|
+
break;
|
|
4713
|
+
} catch (error) {
|
|
4714
|
+
if (retryCount === this.config.retries) {
|
|
4715
|
+
throw error;
|
|
4716
|
+
}
|
|
4717
|
+
retryCount++;
|
|
4718
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
|
|
4719
|
+
}
|
|
4720
|
+
}`;
|
|
4721
|
+
}
|
|
4722
|
+
generateErrorHandlingMethods() {
|
|
4723
|
+
return `
|
|
4724
|
+
/**
|
|
4725
|
+
* Handle error responses from the FHIR server
|
|
4726
|
+
*/
|
|
4727
|
+
private async handleErrorResponse(response: Response): Promise<never> {
|
|
4728
|
+
let operationOutcome: any;
|
|
4729
|
+
|
|
4730
|
+
try {
|
|
4731
|
+
const contentType = response.headers.get('content-type');
|
|
4732
|
+
if (contentType?.includes('application/json') || contentType?.includes('application/fhir+json')) {
|
|
4733
|
+
operationOutcome = await response.json();
|
|
4734
|
+
}
|
|
4735
|
+
} catch {
|
|
4736
|
+
// Ignore JSON parsing errors
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
const error = new Error(\`HTTP \${response.status}: \${response.statusText}\`) as any;
|
|
4740
|
+
error.status = response.status;
|
|
4741
|
+
error.headers = response.headers;
|
|
4742
|
+
error.operationOutcome = operationOutcome;
|
|
4743
|
+
|
|
4744
|
+
throw error;
|
|
4745
|
+
}`;
|
|
4746
|
+
}
|
|
4747
|
+
setOutputDir(directory) {
|
|
4748
|
+
this.options.outputDir = directory;
|
|
4749
|
+
}
|
|
4750
|
+
setOptions(options) {
|
|
4751
|
+
this.options = { ...this.options, ...options };
|
|
4752
|
+
}
|
|
4753
|
+
getOptions() {
|
|
4754
|
+
return { ...this.options };
|
|
4755
|
+
}
|
|
4756
|
+
async generateEnhancedSearchParamsFile() {
|
|
4757
|
+
const content = this.searchParameterEnhancer.generateEnhancedSearchTypes();
|
|
4758
|
+
const baseExports = [
|
|
4759
|
+
"EnhancedSearchParams",
|
|
4760
|
+
"SearchParameterValidator",
|
|
4761
|
+
"SearchModifiers",
|
|
4762
|
+
"BaseEnhancedSearchParams",
|
|
4763
|
+
...Array.from(this.resourceTypes).sort().map((type) => `${type}SearchParams`)
|
|
4764
|
+
];
|
|
4765
|
+
const autocompleteExports = this.options.searchAutocomplete ? [
|
|
4766
|
+
"BaseSearchParamName",
|
|
4767
|
+
"SearchParamName",
|
|
4768
|
+
...Array.from(this.resourceTypes).sort().map((type) => `${type}SearchParamName`)
|
|
4769
|
+
] : [];
|
|
4770
|
+
const valueSetEnumExports = this.options.generateValueSetEnums ? ["PatientGender", "ObservationStatus", "ImmunizationStatus"] : [];
|
|
4771
|
+
return {
|
|
4772
|
+
filename: "enhanced-search-params.ts",
|
|
4773
|
+
content,
|
|
4774
|
+
exports: [...baseExports, ...autocompleteExports, ...valueSetEnumExports]
|
|
4775
|
+
};
|
|
4776
|
+
}
|
|
4777
|
+
generateValidationConfigFields() {
|
|
4778
|
+
if (this.options.includeValidation || this.options.generateValidators) {
|
|
4779
|
+
return `
|
|
4780
|
+
/** Client-side validation options */
|
|
4781
|
+
validation?: {
|
|
4782
|
+
/** Enable client-side validation (default: false) */
|
|
4783
|
+
enabled?: boolean;
|
|
4784
|
+
/** Validation profile to use (default: 'strict') */
|
|
4785
|
+
profile?: 'strict' | 'lenient' | 'minimal';
|
|
4786
|
+
/** Whether to throw on validation errors (default: false) */
|
|
4787
|
+
throwOnError?: boolean;
|
|
4788
|
+
/** Whether to validate before sending requests (default: true) */
|
|
4789
|
+
validateBeforeRequest?: boolean;
|
|
4790
|
+
/** Whether to validate received responses (default: false) */
|
|
4791
|
+
validateResponses?: boolean;
|
|
4792
|
+
};`;
|
|
4793
|
+
}
|
|
4794
|
+
return "";
|
|
4795
|
+
}
|
|
4796
|
+
async generateValidationTypesFile() {
|
|
4797
|
+
const content = this.validationGenerator.generateValidationTypes();
|
|
4798
|
+
return {
|
|
4799
|
+
filename: "validation-types.ts",
|
|
4800
|
+
content,
|
|
4801
|
+
exports: [
|
|
4802
|
+
"ValidationOptions",
|
|
4803
|
+
"ValidationError",
|
|
4804
|
+
"ValidationWarning",
|
|
4805
|
+
"ValidationResult",
|
|
4806
|
+
"ValidationException"
|
|
4807
|
+
]
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
async generateValidatorsFile() {
|
|
4811
|
+
const content = this.validationGenerator.generateResourceValidators();
|
|
4812
|
+
return {
|
|
4813
|
+
filename: "resource-validators.ts",
|
|
4814
|
+
content,
|
|
4815
|
+
exports: [
|
|
4816
|
+
"ResourceValidator",
|
|
4817
|
+
...Array.from(this.resourceTypes).sort().map((type) => `validate${type}`)
|
|
4818
|
+
]
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
generateCRUDMethods() {
|
|
4822
|
+
const validationEnabled = this.options.includeValidation || this.options.generateValidators;
|
|
4823
|
+
return ` /**
|
|
4824
|
+
* Create a new FHIR resource
|
|
4825
|
+
*/
|
|
4826
|
+
async create<T extends ResourceTypes>(
|
|
4827
|
+
resource: ResourceTypeMap[T],
|
|
4828
|
+
options?: RequestOptions
|
|
4829
|
+
): Promise<CreateResponse<ResourceTypeMap[T]>> {
|
|
4830
|
+
const resourceType = resource.resourceType as T;
|
|
4831
|
+
const url = \`\${this.baseUrl}/\${resourceType}\`;
|
|
4832
|
+
|
|
4833
|
+
${validationEnabled ? this.generateValidationCode("create", "resource") : ""}
|
|
4834
|
+
|
|
4835
|
+
return this.request<ResourceTypeMap[T]>('POST', url, resource, options);
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4838
|
+
/**
|
|
4839
|
+
* Read a FHIR resource by ID
|
|
4840
|
+
*/
|
|
4841
|
+
async read<T extends ResourceTypes>(
|
|
4842
|
+
resourceType: T,
|
|
4843
|
+
id: string,
|
|
4844
|
+
options?: RequestOptions
|
|
4845
|
+
): Promise<ReadResponse<ResourceTypeMap[T]>> {
|
|
4846
|
+
const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
|
|
4847
|
+
|
|
4848
|
+
return this.request<ResourceTypeMap[T]>('GET', url, undefined, options);
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
/**
|
|
4852
|
+
* Update a FHIR resource
|
|
4853
|
+
*/
|
|
4854
|
+
async update<T extends ResourceTypes>(
|
|
4855
|
+
resource: ResourceTypeMap[T],
|
|
4856
|
+
options?: RequestOptions
|
|
4857
|
+
): Promise<UpdateResponse<ResourceTypeMap[T]>> {
|
|
4858
|
+
const resourceType = resource.resourceType as T;
|
|
4859
|
+
const id = (resource as any).id;
|
|
4860
|
+
|
|
4861
|
+
if (!id) {
|
|
4862
|
+
throw new Error('Resource must have an id to be updated');
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
|
|
4866
|
+
|
|
4867
|
+
${validationEnabled ? this.generateValidationCode("update", "resource") : ""}
|
|
4868
|
+
|
|
4869
|
+
return this.request<ResourceTypeMap[T]>('PUT', url, resource, options);
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4872
|
+
/**
|
|
4873
|
+
* Delete a FHIR resource
|
|
4874
|
+
*/
|
|
4875
|
+
async delete<T extends ResourceTypes>(
|
|
4876
|
+
resourceType: T,
|
|
4877
|
+
id: string,
|
|
4878
|
+
options?: RequestOptions
|
|
4879
|
+
): Promise<DeleteResponse> {
|
|
4880
|
+
const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
|
|
4881
|
+
|
|
4882
|
+
return this.request<void>('DELETE', url, undefined, options);
|
|
4883
|
+
}`;
|
|
4884
|
+
}
|
|
4885
|
+
generateValidationCode(operation, resourceVar) {
|
|
4886
|
+
return `// Client-side validation if enabled
|
|
4887
|
+
if (this.config.validation?.enabled && this.config.validation?.validateBeforeRequest) {
|
|
4888
|
+
const validationResult = ResourceValidator.validate(${resourceVar}, {
|
|
4889
|
+
profile: this.config.validation.profile || 'strict',
|
|
4890
|
+
throwOnError: this.config.validation.throwOnError || false,
|
|
4891
|
+
validateRequired: true,
|
|
4892
|
+
validateTypes: true,
|
|
4893
|
+
validateConstraints: true
|
|
4894
|
+
});
|
|
4895
|
+
|
|
4896
|
+
if (!validationResult.valid && this.config.validation.throwOnError) {
|
|
4897
|
+
throw new ValidationException(validationResult);
|
|
4898
|
+
} else if (!validationResult.valid) {
|
|
4899
|
+
console.warn(\`Validation warnings for \${operation}:\`, validationResult.errors);
|
|
4900
|
+
}
|
|
4901
|
+
}`;
|
|
4902
|
+
}
|
|
4903
|
+
generateSearchMethod() {
|
|
4904
|
+
if (this.options.enhancedSearch) {
|
|
4905
|
+
const autocompleteOverload = this.options.searchAutocomplete ? `
|
|
4906
|
+
async search<T extends ResourceTypes>(
|
|
4907
|
+
resourceType: T,
|
|
4908
|
+
params?: Partial<Record<SearchParamName<T>, any>> & BaseEnhancedSearchParams,
|
|
4909
|
+
options?: RequestOptions
|
|
4910
|
+
): Promise<SearchResponse<ResourceTypeMap[T]>>;` : "";
|
|
4911
|
+
return ` /**
|
|
4912
|
+
* Search for FHIR resources
|
|
4913
|
+
*/
|
|
4914
|
+
async search<T extends ResourceTypes>(
|
|
4915
|
+
resourceType: T,
|
|
4916
|
+
params?: EnhancedSearchParams<T>,
|
|
4917
|
+
options?: RequestOptions
|
|
4918
|
+
): Promise<SearchResponse<ResourceTypeMap[T]>>;${autocompleteOverload}
|
|
4919
|
+
async search<T extends ResourceTypes>(
|
|
4920
|
+
resourceType: T,
|
|
4921
|
+
params?: SearchParams,
|
|
4922
|
+
options?: RequestOptions
|
|
4923
|
+
): Promise<SearchResponse<ResourceTypeMap[T]>>;
|
|
4924
|
+
async search<T extends ResourceTypes>(
|
|
4925
|
+
resourceType: T,
|
|
4926
|
+
params?: any,
|
|
4927
|
+
options?: RequestOptions
|
|
4928
|
+
): Promise<SearchResponse<ResourceTypeMap[T]>> {
|
|
4929
|
+
let url = \`\${this.baseUrl}/\${resourceType}\`;
|
|
4930
|
+
|
|
4931
|
+
if (params && Object.keys(params).length > 0) {
|
|
4932
|
+
let searchParams: URLSearchParams | undefined;
|
|
4933
|
+
try {
|
|
4934
|
+
const validation = SearchParameterValidator.validate(resourceType, params);
|
|
4935
|
+
if (validation.valid) {
|
|
4936
|
+
searchParams = SearchParameterValidator.buildSearchParams(resourceType, params);
|
|
4937
|
+
}
|
|
4938
|
+
} catch {}
|
|
4939
|
+
if (!searchParams) {
|
|
4940
|
+
searchParams = new URLSearchParams();
|
|
4941
|
+
for (const [key, value] of Object.entries(params)) {
|
|
4942
|
+
if (Array.isArray(value)) {
|
|
4943
|
+
value.forEach((v) => searchParams!.append(key, String(v)));
|
|
4944
|
+
} else if (value !== undefined) {
|
|
4945
|
+
searchParams.append(key, String(value));
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
url += \`?\${searchParams.toString()}\`;
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
return this.request<Bundle<ResourceTypeMap[T]>>('GET', url, undefined, options);
|
|
4953
|
+
}`;
|
|
4954
|
+
}
|
|
4955
|
+
const paramHandling = this.generateSearchParameterHandlingCode();
|
|
4956
|
+
return ` /**
|
|
4957
|
+
* Search for FHIR resources
|
|
4958
|
+
*/
|
|
4959
|
+
async search<T extends ResourceTypes>(
|
|
4960
|
+
resourceType: T,
|
|
4961
|
+
params?: SearchParams,
|
|
4962
|
+
options?: RequestOptions
|
|
4963
|
+
): Promise<SearchResponse<ResourceTypeMap[T]>> {
|
|
4964
|
+
let url = \`\${this.baseUrl}/\${resourceType}\`;
|
|
4965
|
+
|
|
4966
|
+
if (params && Object.keys(params).length > 0) {
|
|
4967
|
+
${paramHandling}
|
|
4968
|
+
url += \`?\${searchParams.toString()}\`;
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4971
|
+
return this.request<Bundle<ResourceTypeMap[T]>>('GET', url, undefined, options);
|
|
4972
|
+
}`;
|
|
4973
|
+
}
|
|
4974
|
+
generateValidationMethods() {
|
|
4975
|
+
if (this.options.includeValidation || this.options.generateValidators) {
|
|
4976
|
+
return `
|
|
4977
|
+
|
|
4978
|
+
/**
|
|
4979
|
+
* Validate a FHIR resource without sending it to the server
|
|
4980
|
+
*/
|
|
4981
|
+
validate<T extends ResourceTypes>(
|
|
4982
|
+
resource: ResourceTypeMap[T],
|
|
4983
|
+
options?: ValidationOptions
|
|
4984
|
+
): ValidationResult {
|
|
4985
|
+
return ResourceValidator.validate(resource, options);
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4988
|
+
/**
|
|
4989
|
+
* Check if validation is enabled for this client
|
|
4990
|
+
*/
|
|
4991
|
+
isValidationEnabled(): boolean {
|
|
4992
|
+
return this.config.validation?.enabled || false;
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
/**
|
|
4996
|
+
* Update validation configuration
|
|
4997
|
+
*/
|
|
4998
|
+
updateValidationConfig(validationConfig: NonNullable<${this.options.clientName}Config['validation']>): void {
|
|
4999
|
+
this.config.validation = { ...this.config.validation, ...validationConfig };
|
|
5000
|
+
}`;
|
|
5001
|
+
}
|
|
5002
|
+
return "";
|
|
5003
|
+
}
|
|
5004
|
+
generateSearchParameterHandlingCode() {
|
|
5005
|
+
if (this.options.enhancedSearch) {
|
|
5006
|
+
return `// Use enhanced search parameter validation and building
|
|
5007
|
+
const validation = SearchParameterValidator.validate(resourceType, params);
|
|
5008
|
+
if (!validation.valid) {
|
|
5009
|
+
throw new Error(\`Invalid search parameters: \${validation.errors.join(', ')}\`);
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
const searchParams = SearchParameterValidator.buildSearchParams(resourceType, params);`;
|
|
5013
|
+
} else {
|
|
5014
|
+
return `const searchParams = new URLSearchParams();
|
|
5015
|
+
for (const [key, value] of Object.entries(params)) {
|
|
5016
|
+
if (Array.isArray(value)) {
|
|
5017
|
+
value.forEach(v => searchParams.append(key, String(v)));
|
|
5018
|
+
} else if (value !== undefined) {
|
|
5019
|
+
searchParams.append(key, String(value));
|
|
5020
|
+
}
|
|
5021
|
+
}`;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
async generateUtilityFile() {
|
|
5025
|
+
const resourceTypesArray = Array.from(this.resourceTypes).sort();
|
|
5026
|
+
const content = `/**
|
|
5027
|
+
* Utility types for FHIR REST Client
|
|
5028
|
+
*
|
|
5029
|
+
* Shared type definitions and utilities.
|
|
5030
|
+
*/
|
|
5031
|
+
|
|
5032
|
+
// Import all the resource types
|
|
5033
|
+
${resourceTypesArray.map((type) => `import type { ${type} } from '../types/${type}';`).join(`
|
|
5034
|
+
`)}
|
|
5035
|
+
|
|
5036
|
+
export type ResourceTypes = ${resourceTypesArray.map((type) => `'${type}'`).join(" | ")};
|
|
5037
|
+
|
|
5038
|
+
/**
|
|
5039
|
+
* Resource type mapping from resource type strings to interfaces
|
|
5040
|
+
*/
|
|
5041
|
+
export type ResourceTypeMap = {
|
|
5042
|
+
${resourceTypesArray.map((type) => ` '${type}': ${type};`).join(`
|
|
5043
|
+
`)}
|
|
5044
|
+
};
|
|
5045
|
+
`;
|
|
5046
|
+
return {
|
|
5047
|
+
filename: "utility.ts",
|
|
5048
|
+
content,
|
|
5049
|
+
exports: ["ResourceTypes", "ResourceTypeMap", ...resourceTypesArray]
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
async ensureDirectoryExists(filePath) {
|
|
5053
|
+
const dir = dirname(filePath);
|
|
5054
|
+
await mkdir2(dir, { recursive: true });
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
// src/api/generators/typescript.ts
|
|
5059
|
+
import { mkdir as mkdir3, writeFile as writeFile5 } from "node:fs/promises";
|
|
5060
|
+
import { dirname as dirname2, join as join11 } from "node:path";
|
|
5061
|
+
|
|
5062
|
+
// src/utils.ts
|
|
5063
|
+
function toPascalCase(input) {
|
|
5064
|
+
const parts = input.replace(/[^A-Za-z0-9]+/g, " ").split(" ").map((p) => p.trim()).filter(Boolean);
|
|
5065
|
+
if (parts.length === 0)
|
|
5066
|
+
return "";
|
|
5067
|
+
return parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
// src/api/generators/typescript.ts
|
|
5071
|
+
var PRIMITIVE_TYPE_MAP = {
|
|
5072
|
+
string: "string",
|
|
5073
|
+
code: "string",
|
|
5074
|
+
uri: "string",
|
|
5075
|
+
url: "string",
|
|
5076
|
+
canonical: "string",
|
|
5077
|
+
oid: "string",
|
|
5078
|
+
uuid: "string",
|
|
5079
|
+
id: "string",
|
|
5080
|
+
markdown: "string",
|
|
5081
|
+
xhtml: "string",
|
|
5082
|
+
base64Binary: "string",
|
|
5083
|
+
integer: "number",
|
|
5084
|
+
unsignedInt: "number",
|
|
5085
|
+
positiveInt: "number",
|
|
5086
|
+
decimal: "number",
|
|
5087
|
+
boolean: "boolean",
|
|
5088
|
+
date: "string",
|
|
5089
|
+
dateTime: "string",
|
|
5090
|
+
instant: "string",
|
|
5091
|
+
time: "string"
|
|
5092
|
+
};
|
|
5093
|
+
|
|
5094
|
+
class TypeScriptAPIGenerator {
|
|
5095
|
+
options;
|
|
5096
|
+
imports = new Map;
|
|
5097
|
+
exports = new Set;
|
|
5098
|
+
resourceTypes = new Set;
|
|
5099
|
+
currentSchemaName;
|
|
5100
|
+
profileTypes = new Set;
|
|
5101
|
+
enumTypes = new Map;
|
|
5102
|
+
globalEnumTypes = new Map;
|
|
5103
|
+
fieldEnumMap = new Map;
|
|
5104
|
+
constructor(options) {
|
|
5105
|
+
this.options = {
|
|
5106
|
+
moduleFormat: "esm",
|
|
5107
|
+
generateIndex: true,
|
|
5108
|
+
includeDocuments: true,
|
|
5109
|
+
namingConvention: "PascalCase",
|
|
5110
|
+
includeExtensions: false,
|
|
5111
|
+
includeProfiles: false,
|
|
5112
|
+
...options
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
async transformSchema(schema) {
|
|
5116
|
+
this.currentSchemaName = this.formatTypeName(schema.identifier.name);
|
|
5117
|
+
if (schema.identifier.kind === "value-set" || schema.identifier.kind === "binding" || schema.identifier.kind === "primitive-type") {
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
if (schema.identifier.kind === "profile") {
|
|
5121
|
+
if (this.options.includeProfiles) {
|
|
5122
|
+
return this.generateProfile(schema);
|
|
5123
|
+
}
|
|
5124
|
+
return;
|
|
5125
|
+
}
|
|
5126
|
+
if (this.currentSchemaName === "Uri") {
|
|
5127
|
+
console.log(schema);
|
|
5128
|
+
}
|
|
5129
|
+
this.imports.clear();
|
|
5130
|
+
this.exports.clear();
|
|
5131
|
+
this.currentSchemaName = this.formatTypeName(schema.identifier.name);
|
|
5132
|
+
const content = this.generateTypeScriptForSchema(schema);
|
|
5133
|
+
for (const enumTypeName of this.enumTypes.keys()) {
|
|
5134
|
+
this.imports.set(enumTypeName, "utility");
|
|
5135
|
+
}
|
|
5136
|
+
const imports = new Map(this.imports);
|
|
5137
|
+
const filename = this.getFilename(schema.identifier);
|
|
5138
|
+
const exports = Array.from(this.exports.keys());
|
|
5139
|
+
return {
|
|
5140
|
+
content,
|
|
5141
|
+
imports,
|
|
5142
|
+
exports,
|
|
5143
|
+
filename
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
generateTypeScriptForSchema(schema) {
|
|
5147
|
+
const lines = [];
|
|
5148
|
+
this.enumTypes.clear();
|
|
5149
|
+
const interfaceName = this.formatTypeName(schema.identifier.name);
|
|
5150
|
+
let overridedName;
|
|
5151
|
+
if (interfaceName === "Reference") {
|
|
5152
|
+
overridedName = `Reference<T extends ResourceTypes = ResourceTypes>`;
|
|
5153
|
+
} else if (interfaceName === "Bundle") {
|
|
5154
|
+
overridedName = `Bundle<T extends keyof ResourceTypeMap = keyof ResourceTypeMap>`;
|
|
5155
|
+
this.imports.set("ResourceTypeMap", "utility");
|
|
5156
|
+
}
|
|
5157
|
+
this.exports.add(interfaceName);
|
|
5158
|
+
const baseInterface = this.getBaseInterface(schema);
|
|
5159
|
+
if (baseInterface && !baseInterface.isPrimitive) {
|
|
5160
|
+
this.imports.set(baseInterface.value, baseInterface.value);
|
|
5161
|
+
lines.push(`export interface ${overridedName ?? interfaceName} extends ${baseInterface.value} {`);
|
|
5162
|
+
} else {
|
|
5163
|
+
lines.push(`export interface ${overridedName ?? interfaceName} {`);
|
|
5164
|
+
}
|
|
5165
|
+
if (schema.identifier.kind === "resource" && interfaceName !== "DomainResource" && interfaceName !== "Resource") {
|
|
5166
|
+
this.resourceTypes.add(interfaceName);
|
|
5167
|
+
lines.push(` resourceType: '${interfaceName}';`);
|
|
5168
|
+
}
|
|
5169
|
+
if (isTypeSchemaForResourceComplexTypeLogical(schema)) {
|
|
5170
|
+
if (schema.fields) {
|
|
5171
|
+
for (const [fieldName, field] of Object.entries(schema.fields)) {
|
|
5172
|
+
const fieldLine = this.generateField(fieldName, field, {
|
|
5173
|
+
isNested: "type" in field && field.type.kind === "nested",
|
|
5174
|
+
baseName: interfaceName
|
|
5175
|
+
});
|
|
5176
|
+
if (fieldLine) {
|
|
5177
|
+
lines.push(` ${fieldLine}`);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
lines.push("}");
|
|
5182
|
+
if (schema.nested) {
|
|
5183
|
+
for (const nested of schema.nested) {
|
|
5184
|
+
lines.push("");
|
|
5185
|
+
lines.push(this.generateNested(this.currentSchemaName ?? "", nested));
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
} else {
|
|
5189
|
+
lines.push("}");
|
|
5190
|
+
}
|
|
5191
|
+
return lines.join(`
|
|
5192
|
+
`);
|
|
5193
|
+
}
|
|
5194
|
+
generateNested(baseName, nested) {
|
|
5195
|
+
const lines = [];
|
|
5196
|
+
const interfaceName = this.formatTypeName(nested.identifier.name);
|
|
5197
|
+
let fullTypeName = `${baseName}${interfaceName}`;
|
|
5198
|
+
if (fullTypeName === "BundleEntry") {
|
|
5199
|
+
fullTypeName = `BundleEntry<T extends keyof ResourceTypeMap = keyof ResourceTypeMap>`;
|
|
5200
|
+
this.imports.set("ResourceTypeMap", "utility");
|
|
5201
|
+
}
|
|
5202
|
+
this.exports.add(baseName + interfaceName);
|
|
5203
|
+
if (nested.base) {
|
|
5204
|
+
const baseInterface = this.getType(nested.base);
|
|
5205
|
+
if (baseInterface.isPrimitive) {
|
|
5206
|
+
lines.push(`export interface ${fullTypeName}{`);
|
|
5207
|
+
} else {
|
|
5208
|
+
this.imports.set(baseInterface.value, baseInterface.value);
|
|
5209
|
+
lines.push(`export interface ${fullTypeName} extends ${baseInterface.value} {`);
|
|
5210
|
+
}
|
|
5211
|
+
} else {
|
|
5212
|
+
lines.push(`export interface ${fullTypeName}{`);
|
|
5213
|
+
}
|
|
5214
|
+
if (nested.fields) {
|
|
5215
|
+
for (const [fieldName, field] of Object.entries(nested.fields)) {
|
|
5216
|
+
const fieldLine = this.generateField(fieldName, field, {
|
|
5217
|
+
isNested: false,
|
|
5218
|
+
baseName: fullTypeName
|
|
5219
|
+
});
|
|
5220
|
+
if (fieldLine) {
|
|
5221
|
+
lines.push(` ${fieldLine}`);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
lines.push("}");
|
|
5226
|
+
return lines.join(`
|
|
5227
|
+
`);
|
|
5228
|
+
}
|
|
5229
|
+
generateField(fieldName, field, nestedOpts) {
|
|
5230
|
+
if (isPolymorphicInstanceField(field)) {
|
|
5231
|
+
return this.generatePolymorphicInstance(fieldName, field, nestedOpts);
|
|
5232
|
+
} else if (isRegularField(field)) {
|
|
5233
|
+
return this.generateRegularField(fieldName, field, nestedOpts);
|
|
5234
|
+
}
|
|
5235
|
+
return "";
|
|
5236
|
+
}
|
|
5237
|
+
generatePolymorphicInstance(fieldName, field, nestedOpts) {
|
|
5238
|
+
let typeString = "any";
|
|
5239
|
+
if (field.enum) {
|
|
5240
|
+
const enumTypeName = this.generateEnumType(fieldName, field.enum, nestedOpts?.baseName);
|
|
5241
|
+
typeString = enumTypeName;
|
|
5242
|
+
} else if (field.reference) {
|
|
5243
|
+
typeString = this.buildReferenceType(field.reference);
|
|
5244
|
+
} else if (field.type) {
|
|
5245
|
+
const subType = this.getType(field.type);
|
|
5246
|
+
if (!subType.isPrimitive && !nestedOpts?.isNested) {
|
|
5247
|
+
this.imports.set(subType.value, subType.value);
|
|
5248
|
+
}
|
|
5249
|
+
typeString = subType.value;
|
|
5250
|
+
}
|
|
5251
|
+
const optional = !field.required ? "?" : "";
|
|
5252
|
+
const array = field.array ? "[]" : "";
|
|
5253
|
+
return `${fieldName}${optional}: ${nestedOpts?.isNested && nestedOpts.baseName ? nestedOpts.baseName : ""}${typeString}${array};`;
|
|
5254
|
+
}
|
|
5255
|
+
generateRegularField(fieldName, field, nestedOpts) {
|
|
5256
|
+
let typeString = "any";
|
|
5257
|
+
if (field.enum) {
|
|
5258
|
+
const enumTypeName = this.generateEnumType(fieldName, field.enum, nestedOpts?.baseName);
|
|
5259
|
+
typeString = enumTypeName;
|
|
5260
|
+
} else if (field.reference) {
|
|
5261
|
+
typeString = this.buildReferenceType(field.reference);
|
|
5262
|
+
} else if (field.type) {
|
|
5263
|
+
const subType = this.getType(field.type);
|
|
5264
|
+
if (!subType.isPrimitive && !nestedOpts?.isNested) {
|
|
5265
|
+
this.imports.set(subType.value, subType.value);
|
|
5266
|
+
}
|
|
5267
|
+
typeString = subType.value;
|
|
5268
|
+
}
|
|
5269
|
+
if (nestedOpts?.baseName === "Reference" && fieldName === "type") {
|
|
5270
|
+
typeString = "T";
|
|
5271
|
+
this.imports.set("ResourceTypes", "utility");
|
|
5272
|
+
} else if (nestedOpts?.baseName?.startsWith("BundleEntry") && fieldName === "resource") {
|
|
5273
|
+
typeString = "ResourceTypeMap[T]";
|
|
5274
|
+
this.imports.set("ResourceTypeMap", "utility");
|
|
5275
|
+
} else if (this.currentSchemaName === "Bundle" && fieldName === "entry") {
|
|
5276
|
+
typeString = "BundleEntry<T>";
|
|
5277
|
+
}
|
|
5278
|
+
const optional = !field.required ? "?" : "";
|
|
5279
|
+
const array = field.array ? "[]" : "";
|
|
5280
|
+
const shouldAddPrefix = nestedOpts?.isNested && nestedOpts.baseName && !(this.currentSchemaName === "Bundle" && fieldName === "entry");
|
|
5281
|
+
return `${fieldName}${optional}: ${shouldAddPrefix ? nestedOpts.baseName : ""}${typeString}${array};`;
|
|
5282
|
+
}
|
|
5283
|
+
buildReferenceType(refers) {
|
|
5284
|
+
this.imports.set("Reference", "Reference");
|
|
5285
|
+
if (refers.length === 0) {
|
|
5286
|
+
return "Reference";
|
|
5287
|
+
}
|
|
5288
|
+
if (refers.length === 1 && refers[0]?.name === "Resource") {
|
|
3151
5289
|
return "Reference";
|
|
3152
5290
|
}
|
|
3153
5291
|
return `Reference<${refers.map((r) => `'${r.name}'`).join(" | ")}>`;
|
|
@@ -3166,31 +5304,51 @@ class TypeScriptAPIGenerator {
|
|
|
3166
5304
|
const filteredSchemas = this.filterSchemas(schemas);
|
|
3167
5305
|
const results = await this.transformSchemas(filteredSchemas);
|
|
3168
5306
|
const generatedFiles = [];
|
|
3169
|
-
await
|
|
5307
|
+
await mkdir3(this.options.outputDir, { recursive: true });
|
|
3170
5308
|
if (this.options.includeProfiles) {
|
|
3171
|
-
await
|
|
5309
|
+
await mkdir3(join11(this.options.outputDir, "profiles"), {
|
|
3172
5310
|
recursive: true
|
|
3173
5311
|
});
|
|
3174
5312
|
}
|
|
3175
5313
|
if (this.options.includeExtensions) {
|
|
3176
|
-
await
|
|
5314
|
+
await mkdir3(join11(this.options.outputDir, "extensions"), {
|
|
3177
5315
|
recursive: true
|
|
3178
5316
|
});
|
|
3179
5317
|
}
|
|
5318
|
+
let utilityContent = `export type ResourceTypes = ${Array.from(this.resourceTypes).map((key) => `'${key}'`).join(" | ")};
|
|
5319
|
+
|
|
5320
|
+
`;
|
|
5321
|
+
if (this.globalEnumTypes.size > 0) {
|
|
5322
|
+
utilityContent += `// Shared Enum Types
|
|
5323
|
+
|
|
5324
|
+
`;
|
|
5325
|
+
for (const [typeName, enumDef] of this.globalEnumTypes) {
|
|
5326
|
+
if (enumDef.description) {
|
|
5327
|
+
utilityContent += `/**
|
|
5328
|
+
* ${enumDef.description}
|
|
5329
|
+
*/
|
|
5330
|
+
`;
|
|
5331
|
+
}
|
|
5332
|
+
const unionType = enumDef.values.map((v) => `'${v}'`).join(" | ");
|
|
5333
|
+
utilityContent += `export type ${typeName} = ${unionType};
|
|
5334
|
+
|
|
5335
|
+
`;
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
3180
5338
|
results.push({
|
|
3181
5339
|
filename: "utility.ts",
|
|
3182
|
-
content:
|
|
3183
|
-
|
|
3184
|
-
`,
|
|
5340
|
+
content: utilityContent,
|
|
3185
5341
|
imports: new Map,
|
|
3186
|
-
exports: ["ResourceTypes"]
|
|
5342
|
+
exports: ["ResourceTypes", ...Array.from(this.globalEnumTypes.keys())]
|
|
3187
5343
|
});
|
|
3188
5344
|
for (const result of results) {
|
|
3189
|
-
const filePath =
|
|
5345
|
+
const filePath = join11(this.options.outputDir, result.filename);
|
|
3190
5346
|
await this.ensureDirectoryExists(filePath);
|
|
3191
|
-
|
|
5347
|
+
const imports = result.filename === "utility.ts" ? "" : this.generateImportStatements(result.imports);
|
|
5348
|
+
const content = imports ? `${imports}
|
|
3192
5349
|
|
|
3193
|
-
${result.content}
|
|
5350
|
+
${result.content}` : result.content;
|
|
5351
|
+
await writeFile5(filePath, content, "utf-8");
|
|
3194
5352
|
generatedFiles.push({
|
|
3195
5353
|
path: filePath,
|
|
3196
5354
|
filename: result.filename,
|
|
@@ -3200,8 +5358,8 @@ ${result.content}`, "utf-8");
|
|
|
3200
5358
|
}
|
|
3201
5359
|
if (this.options.generateIndex) {
|
|
3202
5360
|
const indexFile = await this.generateIndexFile(results);
|
|
3203
|
-
const indexPath =
|
|
3204
|
-
await
|
|
5361
|
+
const indexPath = join11(this.options.outputDir, "index.ts");
|
|
5362
|
+
await writeFile5(indexPath, indexFile.content, "utf-8");
|
|
3205
5363
|
generatedFiles.push({
|
|
3206
5364
|
path: indexPath,
|
|
3207
5365
|
filename: "index.ts",
|
|
@@ -3214,8 +5372,22 @@ ${result.content}`, "utf-8");
|
|
|
3214
5372
|
}
|
|
3215
5373
|
generateImportStatements(imports) {
|
|
3216
5374
|
const lines = [];
|
|
3217
|
-
|
|
3218
|
-
|
|
5375
|
+
const importsByPackage = new Map;
|
|
5376
|
+
for (const [item, pkg] of imports) {
|
|
5377
|
+
if (!importsByPackage.has(pkg)) {
|
|
5378
|
+
importsByPackage.set(pkg, []);
|
|
5379
|
+
}
|
|
5380
|
+
importsByPackage.get(pkg).push(item);
|
|
5381
|
+
}
|
|
5382
|
+
for (const [pkg, items] of importsByPackage) {
|
|
5383
|
+
if (items.length === 1) {
|
|
5384
|
+
lines.push(`import type { ${items[0]} } from './${pkg}';`);
|
|
5385
|
+
} else {
|
|
5386
|
+
lines.push(`import type {
|
|
5387
|
+
${items.join(`,
|
|
5388
|
+
`)}
|
|
5389
|
+
} from './${pkg}';`);
|
|
5390
|
+
}
|
|
3219
5391
|
}
|
|
3220
5392
|
return lines.join(`
|
|
3221
5393
|
`);
|
|
@@ -3226,7 +5398,7 @@ ${result.content}`, "utf-8");
|
|
|
3226
5398
|
const generatedFiles = [];
|
|
3227
5399
|
for (const result of results) {
|
|
3228
5400
|
generatedFiles.push({
|
|
3229
|
-
path:
|
|
5401
|
+
path: join11(this.options.outputDir, result.filename),
|
|
3230
5402
|
filename: result.filename,
|
|
3231
5403
|
content: result.content,
|
|
3232
5404
|
exports: result.exports
|
|
@@ -3235,7 +5407,7 @@ ${result.content}`, "utf-8");
|
|
|
3235
5407
|
if (this.options.generateIndex) {
|
|
3236
5408
|
const indexFile = await this.generateIndexFile(results);
|
|
3237
5409
|
generatedFiles.push({
|
|
3238
|
-
path:
|
|
5410
|
+
path: join11(this.options.outputDir, "index.ts"),
|
|
3239
5411
|
filename: "index.ts",
|
|
3240
5412
|
content: indexFile.content,
|
|
3241
5413
|
exports: indexFile.exports
|
|
@@ -3270,14 +5442,14 @@ ${result.content}`, "utf-8");
|
|
|
3270
5442
|
const valueSets = [];
|
|
3271
5443
|
for (const result of results) {
|
|
3272
5444
|
for (const exportName of result.exports) {
|
|
3273
|
-
if (result.filename.includes("
|
|
5445
|
+
if (result.filename.includes("profiles/")) {
|
|
5446
|
+
profiles.push(exportName);
|
|
5447
|
+
} else if (result.filename.includes("primitive")) {
|
|
3274
5448
|
primitiveTypes.push(exportName);
|
|
3275
5449
|
} else if (result.filename.includes("complex")) {
|
|
3276
5450
|
complexTypes.push(exportName);
|
|
3277
5451
|
} else if (result.filename.includes("resource")) {
|
|
3278
5452
|
resources.push(exportName);
|
|
3279
|
-
} else if (result.filename.includes("profile")) {
|
|
3280
|
-
profiles.push(exportName);
|
|
3281
5453
|
} else if (result.filename.includes("valueset")) {
|
|
3282
5454
|
valueSets.push(exportName);
|
|
3283
5455
|
} else {
|
|
@@ -3285,7 +5457,7 @@ ${result.content}`, "utf-8");
|
|
|
3285
5457
|
}
|
|
3286
5458
|
}
|
|
3287
5459
|
const baseName = result.filename.replace(".ts", "");
|
|
3288
|
-
if (baseName.startsWith("valueset") || baseName.startsWith("extension") || baseName.startsWith("
|
|
5460
|
+
if (baseName.startsWith("valueset") || baseName.startsWith("extension") || baseName.startsWith("profiles/")) {
|
|
3289
5461
|
continue;
|
|
3290
5462
|
}
|
|
3291
5463
|
if (result.exports.length > 0) {
|
|
@@ -3335,103 +5507,463 @@ ${result.content}`, "utf-8");
|
|
|
3335
5507
|
lines.push("// Value sets namespace - contains all FHIR ValueSet types and bindings");
|
|
3336
5508
|
lines.push('export * as ValueSets from "./valuesets";');
|
|
3337
5509
|
lines.push("");
|
|
3338
|
-
if (this.options.includeExtensions) {
|
|
3339
|
-
lines.push("// Extensions namespace - contains all FHIR Extension types");
|
|
3340
|
-
lines.push('export * as Extensions from "./extensions";');
|
|
5510
|
+
if (this.options.includeExtensions) {
|
|
5511
|
+
lines.push("// Extensions namespace - contains all FHIR Extension types");
|
|
5512
|
+
lines.push('export * as Extensions from "./extensions";');
|
|
5513
|
+
lines.push("");
|
|
5514
|
+
}
|
|
5515
|
+
if (this.options.includeProfiles) {
|
|
5516
|
+
lines.push("// Profiles namespace - contains all FHIR Profile types");
|
|
5517
|
+
lines.push('export * as Profiles from "./profiles";');
|
|
5518
|
+
lines.push("");
|
|
5519
|
+
}
|
|
5520
|
+
const content = lines.join(`
|
|
5521
|
+
`);
|
|
5522
|
+
return { content, exports };
|
|
5523
|
+
}
|
|
5524
|
+
async ensureDirectoryExists(filePath) {
|
|
5525
|
+
const dir = dirname2(filePath);
|
|
5526
|
+
await mkdir3(dir, { recursive: true });
|
|
5527
|
+
}
|
|
5528
|
+
filterSchemas(schemas) {
|
|
5529
|
+
if (this.options.includeExtensions) {
|
|
5530
|
+
return schemas;
|
|
5531
|
+
}
|
|
5532
|
+
return schemas;
|
|
5533
|
+
}
|
|
5534
|
+
async generateSubfolderIndexFiles(results, generatedFiles) {
|
|
5535
|
+
const subfolders = [];
|
|
5536
|
+
if (this.options.includeExtensions) {
|
|
5537
|
+
subfolders.push("extensions");
|
|
5538
|
+
}
|
|
5539
|
+
if (this.options.includeProfiles) {
|
|
5540
|
+
subfolders.push("profiles");
|
|
5541
|
+
}
|
|
5542
|
+
for (const subfolder of subfolders) {
|
|
5543
|
+
const subfolderResults = results.filter((r) => r.filename.startsWith(`${subfolder}/`));
|
|
5544
|
+
const indexContent = this.generateSubfolderIndex(subfolderResults, subfolder);
|
|
5545
|
+
const indexPath = join11(this.options.outputDir, subfolder, "index.ts");
|
|
5546
|
+
await writeFile5(indexPath, indexContent, "utf-8");
|
|
5547
|
+
generatedFiles.push({
|
|
5548
|
+
path: indexPath,
|
|
5549
|
+
filename: `${subfolder}/index.ts`,
|
|
5550
|
+
content: indexContent,
|
|
5551
|
+
exports: subfolderResults.flatMap((r) => r.exports)
|
|
5552
|
+
});
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
generateSubfolderIndex(results, subfolder) {
|
|
5556
|
+
const lines = [];
|
|
5557
|
+
lines.push("/**");
|
|
5558
|
+
lines.push(` * ${subfolder.charAt(0).toUpperCase() + subfolder.slice(1)} Index`);
|
|
5559
|
+
lines.push(" * ");
|
|
5560
|
+
lines.push(" * Auto-generated exports for all types in this subfolder.");
|
|
5561
|
+
lines.push(" */");
|
|
5562
|
+
lines.push("");
|
|
5563
|
+
if (results.length === 0) {
|
|
5564
|
+
lines.push("// No types in this category");
|
|
5565
|
+
lines.push("export {};");
|
|
5566
|
+
return lines.join(`
|
|
5567
|
+
`);
|
|
5568
|
+
}
|
|
5569
|
+
for (const result of results) {
|
|
5570
|
+
const baseName = result.filename.replace(`${subfolder}/`, "").replace(".ts", "");
|
|
5571
|
+
if (result.exports.length > 0) {
|
|
5572
|
+
if (result.exports.length === 1) {
|
|
5573
|
+
lines.push(`export type { ${result.exports[0]} } from './${baseName}';`);
|
|
5574
|
+
} else {
|
|
5575
|
+
lines.push(`export type {`);
|
|
5576
|
+
for (let i = 0;i < result.exports.length; i++) {
|
|
5577
|
+
const exportName = result.exports[i];
|
|
5578
|
+
const isLast = i === result.exports.length - 1;
|
|
5579
|
+
lines.push(` ${exportName}${isLast ? "" : ","}`);
|
|
5580
|
+
}
|
|
5581
|
+
lines.push(`} from './${baseName}';`);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
return lines.join(`
|
|
5586
|
+
`);
|
|
5587
|
+
}
|
|
5588
|
+
formatTypeName(name) {
|
|
5589
|
+
if (this.options.namingConvention === "PascalCase") {
|
|
5590
|
+
return toPascalCase(name);
|
|
5591
|
+
}
|
|
5592
|
+
return name;
|
|
5593
|
+
}
|
|
5594
|
+
getType(identifier) {
|
|
5595
|
+
const primitiveType = PRIMITIVE_TYPE_MAP[identifier.name];
|
|
5596
|
+
if (primitiveType) {
|
|
5597
|
+
return { isPrimitive: true, value: primitiveType };
|
|
5598
|
+
}
|
|
5599
|
+
return { isPrimitive: false, value: this.formatTypeName(identifier.name) };
|
|
5600
|
+
}
|
|
5601
|
+
getBaseInterface(schema) {
|
|
5602
|
+
if ((isTypeSchemaForResourceComplexTypeLogical(schema) || schema.identifier.kind === "profile") && schema.base) {
|
|
5603
|
+
return this.getType(schema.base);
|
|
5604
|
+
}
|
|
5605
|
+
return null;
|
|
5606
|
+
}
|
|
5607
|
+
getFilename(identifier) {
|
|
5608
|
+
const name = toPascalCase(identifier.name);
|
|
5609
|
+
if (identifier.kind === "profile" && this.options.includeProfiles) {
|
|
5610
|
+
const subfolder = "profiles";
|
|
5611
|
+
return `${subfolder}/${name}.ts`;
|
|
5612
|
+
}
|
|
5613
|
+
return `${name}.ts`;
|
|
5614
|
+
}
|
|
5615
|
+
async generateProfile(schema) {
|
|
5616
|
+
this.imports.clear();
|
|
5617
|
+
this.exports.clear();
|
|
5618
|
+
this.currentSchemaName = this.formatTypeName(schema.identifier.name);
|
|
5619
|
+
this.profileTypes.add(this.currentSchemaName);
|
|
5620
|
+
const content = this.generateTypeScriptForProfile(schema);
|
|
5621
|
+
const imports = new Map(this.imports);
|
|
5622
|
+
const filename = this.getFilename(schema.identifier);
|
|
5623
|
+
const exports = Array.from(this.exports.keys());
|
|
5624
|
+
return {
|
|
5625
|
+
content,
|
|
5626
|
+
imports,
|
|
5627
|
+
exports,
|
|
5628
|
+
filename
|
|
5629
|
+
};
|
|
5630
|
+
}
|
|
5631
|
+
generateTypeScriptForProfile(schema) {
|
|
5632
|
+
const lines = [];
|
|
5633
|
+
const interfaceName = this.formatTypeName(schema.identifier.name);
|
|
5634
|
+
if (schema.description) {
|
|
5635
|
+
lines.push("/**");
|
|
5636
|
+
lines.push(` * ${schema.description}`);
|
|
5637
|
+
lines.push(` * @profile ${schema.identifier.url}`);
|
|
5638
|
+
lines.push(" */");
|
|
5639
|
+
}
|
|
5640
|
+
this.exports.add(interfaceName);
|
|
5641
|
+
const baseInterface = this.getBaseInterface(schema);
|
|
5642
|
+
if (baseInterface && !baseInterface.isPrimitive) {
|
|
5643
|
+
const importPath = baseInterface.value;
|
|
5644
|
+
this.imports.set(baseInterface.value, `../${importPath}`);
|
|
5645
|
+
lines.push(`export interface ${interfaceName} extends ${baseInterface.value} {`);
|
|
5646
|
+
} else {
|
|
5647
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
5648
|
+
}
|
|
5649
|
+
if (isTypeSchemaForResourceComplexTypeLogical(schema) || schema.identifier.kind === "profile") {
|
|
5650
|
+
if (schema.constraints && Object.keys(schema.constraints).length > 0) {
|
|
5651
|
+
for (const [path9, constraint] of Object.entries(schema.constraints)) {
|
|
5652
|
+
const fieldName = path9.includes(".") ? path9.split(".").pop() : path9;
|
|
5653
|
+
if (!fieldName)
|
|
5654
|
+
continue;
|
|
5655
|
+
if (constraint.min && constraint.min > 0) {
|
|
5656
|
+
const field = schema.fields?.[fieldName];
|
|
5657
|
+
if (field) {
|
|
5658
|
+
const fieldLine = this.generateProfileField(fieldName, field, schema);
|
|
5659
|
+
if (fieldLine) {
|
|
5660
|
+
lines.push(` ${fieldLine}`);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
lines.push("}");
|
|
5667
|
+
if (schema.nested) {
|
|
5668
|
+
for (const nested of schema.nested) {
|
|
5669
|
+
lines.push("");
|
|
5670
|
+
lines.push(this.generateNested(this.currentSchemaName ?? "", nested));
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
} else {
|
|
5674
|
+
lines.push("}");
|
|
5675
|
+
}
|
|
5676
|
+
if (this.options.includeProfiles) {
|
|
5677
|
+
lines.push("");
|
|
5678
|
+
lines.push(this.generateProfileValidator(interfaceName, schema));
|
|
5679
|
+
}
|
|
5680
|
+
lines.push("");
|
|
5681
|
+
lines.push(this.generateProfileTypeGuard(interfaceName, schema));
|
|
5682
|
+
return lines.join(`
|
|
5683
|
+
`);
|
|
5684
|
+
}
|
|
5685
|
+
generateProfileField(fieldName, field, schema) {
|
|
5686
|
+
let typeString = "any";
|
|
5687
|
+
let required = false;
|
|
5688
|
+
let array = false;
|
|
5689
|
+
if (isRegularField(field)) {
|
|
5690
|
+
required = field.required || false;
|
|
5691
|
+
array = field.array || false;
|
|
5692
|
+
if (field.enum) {
|
|
5693
|
+
const enumTypeName = this.generateEnumType(fieldName, field.enum, this.currentSchemaName);
|
|
5694
|
+
typeString = enumTypeName;
|
|
5695
|
+
} else if (field.reference) {
|
|
5696
|
+
typeString = this.buildReferenceType(field.reference);
|
|
5697
|
+
} else if (field.type) {
|
|
5698
|
+
const subType = this.getType(field.type);
|
|
5699
|
+
if (!subType.isPrimitive) {
|
|
5700
|
+
this.imports.set(subType.value, `../${subType.value}`);
|
|
5701
|
+
}
|
|
5702
|
+
typeString = subType.value;
|
|
5703
|
+
}
|
|
5704
|
+
} else if (isPolymorphicInstanceField(field)) {
|
|
5705
|
+
required = field.required || false;
|
|
5706
|
+
array = field.array || false;
|
|
5707
|
+
if (field.reference) {
|
|
5708
|
+
typeString = this.buildReferenceType(field.reference);
|
|
5709
|
+
} else if (field.type) {
|
|
5710
|
+
const subType = this.getType(field.type);
|
|
5711
|
+
if (!subType.isPrimitive) {
|
|
5712
|
+
this.imports.set(subType.value, `../${subType.value}`);
|
|
5713
|
+
}
|
|
5714
|
+
typeString = subType.value;
|
|
5715
|
+
}
|
|
5716
|
+
} else if ("choices" in field) {
|
|
5717
|
+
required = field.required || false;
|
|
5718
|
+
array = field.array || false;
|
|
5719
|
+
return "";
|
|
5720
|
+
}
|
|
5721
|
+
if (schema.constraints && schema.constraints[fieldName]) {
|
|
5722
|
+
const constraint = schema.constraints[fieldName];
|
|
5723
|
+
if (constraint.min && constraint.min > 0) {
|
|
5724
|
+
required = true;
|
|
5725
|
+
}
|
|
5726
|
+
if (constraint.max === "*") {
|
|
5727
|
+
array = true;
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
const optional = !required ? "?" : "";
|
|
5731
|
+
const arrayType = array ? "[]" : "";
|
|
5732
|
+
return `${fieldName}${optional}: ${typeString}${arrayType};`;
|
|
5733
|
+
}
|
|
5734
|
+
generateProfileValidator(interfaceName, schema) {
|
|
5735
|
+
const lines = [];
|
|
5736
|
+
const validatorName = `validate${interfaceName}`;
|
|
5737
|
+
lines.push("/**");
|
|
5738
|
+
lines.push(` * Validate a resource against the ${interfaceName} profile`);
|
|
5739
|
+
lines.push(" */");
|
|
5740
|
+
lines.push(`export function ${validatorName}(resource: unknown): ValidationResult {`);
|
|
5741
|
+
lines.push("\tconst errors: string[] = [];");
|
|
5742
|
+
lines.push("");
|
|
5743
|
+
const baseInterface = this.getBaseInterface(schema);
|
|
5744
|
+
if (baseInterface && !baseInterface.isPrimitive) {
|
|
5745
|
+
this.imports.set(`is${baseInterface.value}`, `../${baseInterface.value}`);
|
|
5746
|
+
lines.push(` // Validate base resource`);
|
|
5747
|
+
lines.push(` if (!is${baseInterface.value}(resource)) {`);
|
|
5748
|
+
lines.push(` return { valid: false, errors: ['Not a valid ${baseInterface.value} resource'] };`);
|
|
5749
|
+
lines.push("\t}");
|
|
5750
|
+
lines.push("");
|
|
5751
|
+
lines.push(` const typed = resource as ${baseInterface.value};`);
|
|
3341
5752
|
lines.push("");
|
|
3342
5753
|
}
|
|
3343
|
-
if (
|
|
3344
|
-
lines.push("//
|
|
3345
|
-
|
|
5754
|
+
if (schema.constraints) {
|
|
5755
|
+
lines.push("\t// Profile constraint validation");
|
|
5756
|
+
for (const [path9, constraint] of Object.entries(schema.constraints)) {
|
|
5757
|
+
this.generateConstraintValidation(lines, path9, constraint);
|
|
5758
|
+
}
|
|
3346
5759
|
lines.push("");
|
|
3347
5760
|
}
|
|
3348
|
-
|
|
5761
|
+
if (schema.constraints) {
|
|
5762
|
+
const mustSupportFields = [];
|
|
5763
|
+
for (const [path9, constraint] of Object.entries(schema.constraints)) {
|
|
5764
|
+
if (constraint.mustSupport) {
|
|
5765
|
+
mustSupportFields.push(path9);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
if (mustSupportFields.length > 0) {
|
|
5769
|
+
lines.push("\t// Must Support elements validation");
|
|
5770
|
+
for (const field of mustSupportFields) {
|
|
5771
|
+
lines.push(` // Must support: ${field}`);
|
|
5772
|
+
lines.push(` // Note: Must Support validation is context-dependent`);
|
|
5773
|
+
}
|
|
5774
|
+
lines.push("");
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
lines.push("\treturn {");
|
|
5778
|
+
lines.push("\t\tvalid: errors.length === 0,");
|
|
5779
|
+
lines.push("\t\terrors");
|
|
5780
|
+
lines.push("\t};");
|
|
5781
|
+
lines.push("}");
|
|
5782
|
+
this.imports.set("ValidationResult", "../types");
|
|
5783
|
+
return lines.join(`
|
|
3349
5784
|
`);
|
|
3350
|
-
return { content, exports };
|
|
3351
|
-
}
|
|
3352
|
-
async ensureDirectoryExists(filePath) {
|
|
3353
|
-
const dir = dirname(filePath);
|
|
3354
|
-
await mkdir2(dir, { recursive: true });
|
|
3355
5785
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
5786
|
+
generateConstrainedProfileField(path9, constraint, schema) {
|
|
5787
|
+
const fieldPath = path9.includes(".") ? path9.split(".").pop() : path9;
|
|
5788
|
+
if (!fieldPath)
|
|
5789
|
+
return "";
|
|
5790
|
+
let typeString = "any";
|
|
5791
|
+
let required = false;
|
|
5792
|
+
let array = false;
|
|
5793
|
+
if (constraint.min !== undefined && constraint.min > 0) {
|
|
5794
|
+
required = true;
|
|
3359
5795
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
async generateSubfolderIndexFiles(results, generatedFiles) {
|
|
3363
|
-
const subfolders = [];
|
|
3364
|
-
if (this.options.includeExtensions) {
|
|
3365
|
-
subfolders.push("extensions");
|
|
5796
|
+
if (constraint.max === "*") {
|
|
5797
|
+
array = true;
|
|
3366
5798
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
5799
|
+
switch (fieldPath) {
|
|
5800
|
+
case "status":
|
|
5801
|
+
typeString = "string";
|
|
5802
|
+
break;
|
|
5803
|
+
case "category":
|
|
5804
|
+
typeString = "CodeableConcept";
|
|
5805
|
+
array = true;
|
|
5806
|
+
this.imports.set("CodeableConcept", "../CodeableConcept");
|
|
5807
|
+
break;
|
|
5808
|
+
case "code":
|
|
5809
|
+
typeString = "CodeableConcept";
|
|
5810
|
+
this.imports.set("CodeableConcept", "../CodeableConcept");
|
|
5811
|
+
break;
|
|
5812
|
+
case "subject":
|
|
5813
|
+
typeString = "Reference";
|
|
5814
|
+
this.imports.set("Reference", "../Reference");
|
|
5815
|
+
break;
|
|
5816
|
+
case "effective":
|
|
5817
|
+
case "effectiveDateTime":
|
|
5818
|
+
typeString = "string";
|
|
5819
|
+
break;
|
|
5820
|
+
case "effectivePeriod":
|
|
5821
|
+
typeString = "Period";
|
|
5822
|
+
this.imports.set("Period", "../Period");
|
|
5823
|
+
break;
|
|
5824
|
+
case "dataAbsentReason":
|
|
5825
|
+
typeString = "CodeableConcept";
|
|
5826
|
+
this.imports.set("CodeableConcept", "../CodeableConcept");
|
|
5827
|
+
break;
|
|
5828
|
+
case "component":
|
|
5829
|
+
typeString = "any";
|
|
5830
|
+
array = true;
|
|
5831
|
+
break;
|
|
5832
|
+
default:
|
|
5833
|
+
if (constraint.binding && constraint.binding.strength === "required") {
|
|
5834
|
+
typeString = "string";
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
if (constraint.fixedValue !== undefined) {
|
|
5838
|
+
if (typeof constraint.fixedValue === "string") {
|
|
5839
|
+
typeString = `'${constraint.fixedValue}'`;
|
|
5840
|
+
} else {
|
|
5841
|
+
typeString = JSON.stringify(constraint.fixedValue);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
const optional = !required ? "?" : "";
|
|
5845
|
+
const arrayType = array ? "[]" : "";
|
|
5846
|
+
return `${fieldPath}${optional}: ${typeString}${arrayType};`;
|
|
5847
|
+
}
|
|
5848
|
+
generateConstraintValidation(lines, path9, constraint) {
|
|
5849
|
+
const fieldPath = path9.includes(".") ? path9.split(".").pop() : path9;
|
|
5850
|
+
if (constraint.min !== undefined && constraint.min > 0) {
|
|
5851
|
+
if (constraint.min === 1) {
|
|
5852
|
+
lines.push(` if (!typed.${fieldPath}) {`);
|
|
5853
|
+
lines.push(` errors.push('${fieldPath} is required for this profile');`);
|
|
5854
|
+
lines.push("\t}");
|
|
5855
|
+
} else {
|
|
5856
|
+
lines.push(` if (!typed.${fieldPath} || (Array.isArray(typed.${fieldPath}) && typed.${fieldPath}.length < ${constraint.min})) {`);
|
|
5857
|
+
lines.push(` errors.push('${fieldPath} must have at least ${constraint.min} item(s)');`);
|
|
5858
|
+
lines.push("\t}");
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
if (constraint.fixedValue !== undefined) {
|
|
5862
|
+
const fixedValue = typeof constraint.fixedValue === "string" ? `'${constraint.fixedValue}'` : JSON.stringify(constraint.fixedValue);
|
|
5863
|
+
lines.push(` if (typed.${fieldPath} !== ${fixedValue}) {`);
|
|
5864
|
+
lines.push(` errors.push('${fieldPath} must have fixed value ${fixedValue}');`);
|
|
5865
|
+
lines.push("\t}");
|
|
5866
|
+
}
|
|
5867
|
+
if (constraint.binding && constraint.binding.strength === "required") {
|
|
5868
|
+
lines.push(` // Required binding validation for ${fieldPath}`);
|
|
5869
|
+
lines.push(` // Note: Full value set validation requires external value set service`);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
generateEnumType(fieldName, enumValues, baseName) {
|
|
5873
|
+
const prefix = baseName || this.currentSchemaName || "";
|
|
5874
|
+
const enumTypeName = `${prefix}${this.toPascalCase(fieldName)}Values`;
|
|
5875
|
+
if (!this.enumTypes.has(enumTypeName)) {
|
|
5876
|
+
this.enumTypes.set(enumTypeName, {
|
|
5877
|
+
values: enumValues,
|
|
5878
|
+
description: `Valid values for ${fieldName} field in ${prefix}`
|
|
3377
5879
|
});
|
|
5880
|
+
this.exports.add(enumTypeName);
|
|
5881
|
+
if (!this.globalEnumTypes.has(enumTypeName)) {
|
|
5882
|
+
this.globalEnumTypes.set(enumTypeName, {
|
|
5883
|
+
values: enumValues,
|
|
5884
|
+
description: `Valid values for ${fieldName} field in ${prefix}`
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
3378
5887
|
}
|
|
5888
|
+
return enumTypeName;
|
|
3379
5889
|
}
|
|
3380
|
-
|
|
5890
|
+
generateEnumTypes() {
|
|
3381
5891
|
const lines = [];
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
lines.push("export {};");
|
|
3391
|
-
return lines.join(`
|
|
3392
|
-
`);
|
|
3393
|
-
}
|
|
3394
|
-
for (const result of results) {
|
|
3395
|
-
const baseName = result.filename.replace(`${subfolder}/`, "").replace(".ts", "");
|
|
3396
|
-
if (result.exports.length > 0) {
|
|
3397
|
-
if (result.exports.length === 1) {
|
|
3398
|
-
lines.push(`export type { ${result.exports[0]} } from './${baseName}';`);
|
|
3399
|
-
} else {
|
|
3400
|
-
lines.push(`export type {`);
|
|
3401
|
-
for (let i = 0;i < result.exports.length; i++) {
|
|
3402
|
-
const exportName = result.exports[i];
|
|
3403
|
-
const isLast = i === result.exports.length - 1;
|
|
3404
|
-
lines.push(` ${exportName}${isLast ? "" : ","}`);
|
|
3405
|
-
}
|
|
3406
|
-
lines.push(`} from './${baseName}';`);
|
|
5892
|
+
if (this.enumTypes.size > 0) {
|
|
5893
|
+
lines.push("// Enum Types");
|
|
5894
|
+
lines.push("");
|
|
5895
|
+
for (const [typeName, enumDef] of this.enumTypes) {
|
|
5896
|
+
if (enumDef.description) {
|
|
5897
|
+
lines.push("/**");
|
|
5898
|
+
lines.push(` * ${enumDef.description}`);
|
|
5899
|
+
lines.push(" */");
|
|
3407
5900
|
}
|
|
5901
|
+
const unionType = enumDef.values.map((v) => `'${v}'`).join(" | ");
|
|
5902
|
+
lines.push(`export type ${typeName} = ${unionType};`);
|
|
5903
|
+
lines.push("");
|
|
3408
5904
|
}
|
|
3409
5905
|
}
|
|
3410
5906
|
return lines.join(`
|
|
3411
5907
|
`);
|
|
3412
5908
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
return toPascalCase(name);
|
|
3416
|
-
}
|
|
3417
|
-
return name;
|
|
5909
|
+
toPascalCase(str) {
|
|
5910
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3418
5911
|
}
|
|
3419
|
-
|
|
3420
|
-
const
|
|
3421
|
-
|
|
3422
|
-
|
|
5912
|
+
generateProfileTypeGuard(interfaceName, schema) {
|
|
5913
|
+
const lines = [];
|
|
5914
|
+
const guardName = `is${interfaceName}`;
|
|
5915
|
+
lines.push("/**");
|
|
5916
|
+
lines.push(` * Type guard for ${interfaceName} profile`);
|
|
5917
|
+
lines.push(" */");
|
|
5918
|
+
lines.push(`export function ${guardName}(resource: unknown): resource is ${interfaceName} {`);
|
|
5919
|
+
const baseInterface = this.getBaseInterface(schema);
|
|
5920
|
+
if (baseInterface && !baseInterface.isPrimitive) {
|
|
5921
|
+
this.imports.set(`is${baseInterface.value}`, `../${baseInterface.value}`);
|
|
5922
|
+
lines.push(` if (!is${baseInterface.value}(resource)) return false;`);
|
|
5923
|
+
lines.push("");
|
|
5924
|
+
lines.push(` const typed = resource as ${baseInterface.value};`);
|
|
5925
|
+
} else {
|
|
5926
|
+
lines.push(` if (!resource || typeof resource !== 'object') return false;`);
|
|
5927
|
+
lines.push("");
|
|
5928
|
+
lines.push("\tconst typed = resource as any;");
|
|
5929
|
+
}
|
|
5930
|
+
if (schema.constraints) {
|
|
5931
|
+
lines.push("\t// Profile constraint checks");
|
|
5932
|
+
let hasRequiredChecks = false;
|
|
5933
|
+
for (const [path9, constraint] of Object.entries(schema.constraints)) {
|
|
5934
|
+
const fieldPath = path9.includes(".") ? path9.split(".").pop() : path9;
|
|
5935
|
+
if (constraint.min !== undefined && constraint.min > 0) {
|
|
5936
|
+
hasRequiredChecks = true;
|
|
5937
|
+
if (constraint.min === 1) {
|
|
5938
|
+
lines.push(` if (!typed.${fieldPath}) return false;`);
|
|
5939
|
+
} else {
|
|
5940
|
+
lines.push(` if (!typed.${fieldPath} || (Array.isArray(typed.${fieldPath}) && typed.${fieldPath}.length < ${constraint.min})) return false;`);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
if (constraint.fixedValue !== undefined) {
|
|
5944
|
+
hasRequiredChecks = true;
|
|
5945
|
+
const fixedValue = typeof constraint.fixedValue === "string" ? `'${constraint.fixedValue}'` : JSON.stringify(constraint.fixedValue);
|
|
5946
|
+
lines.push(` if (typed.${fieldPath} !== ${fixedValue}) return false;`);
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
if (hasRequiredChecks) {
|
|
5950
|
+
lines.push("");
|
|
5951
|
+
}
|
|
3423
5952
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
5953
|
+
if (schema.extensions && schema.extensions.length > 0) {
|
|
5954
|
+
lines.push("\t// Extension presence checks (simplified)");
|
|
5955
|
+
for (const ext of schema.extensions) {
|
|
5956
|
+
if (ext.min && ext.min > 0) {
|
|
5957
|
+
lines.push(` // Required extension: ${ext.profile}`);
|
|
5958
|
+
lines.push(` // Note: Full extension validation requires examining extension array`);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
lines.push("");
|
|
3429
5962
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
return `${name}.ts`;
|
|
5963
|
+
lines.push("\treturn true;");
|
|
5964
|
+
lines.push("}");
|
|
5965
|
+
return lines.join(`
|
|
5966
|
+
`);
|
|
3435
5967
|
}
|
|
3436
5968
|
}
|
|
3437
5969
|
|
|
@@ -3502,6 +6034,23 @@ class APIBuilder {
|
|
|
3502
6034
|
}, "typescript");
|
|
3503
6035
|
return this;
|
|
3504
6036
|
}
|
|
6037
|
+
restClient(options = {}) {
|
|
6038
|
+
const clientOutputDir = `${this.options.outputDir}/client`;
|
|
6039
|
+
const generator = new RestClientGenerator({
|
|
6040
|
+
outputDir: clientOutputDir,
|
|
6041
|
+
...options
|
|
6042
|
+
});
|
|
6043
|
+
this.generators.set("restclient", generator);
|
|
6044
|
+
this.logger.info("Configured REST client generator", {
|
|
6045
|
+
outputDir: clientOutputDir,
|
|
6046
|
+
clientName: options.clientName || "FHIRClient",
|
|
6047
|
+
includeValidation: options.includeValidation ?? false,
|
|
6048
|
+
includeErrorHandling: options.includeErrorHandling ?? true,
|
|
6049
|
+
enhancedSearch: options.enhancedSearch ?? false,
|
|
6050
|
+
useCanonicalManager: options.useCanonicalManager ?? true
|
|
6051
|
+
}, "restclient");
|
|
6052
|
+
return this;
|
|
6053
|
+
}
|
|
3505
6054
|
onProgress(callback) {
|
|
3506
6055
|
this.progressCallback = callback;
|
|
3507
6056
|
return this;
|
|
@@ -3524,7 +6073,21 @@ class APIBuilder {
|
|
|
3524
6073
|
this.options.validate = enabled;
|
|
3525
6074
|
return this;
|
|
3526
6075
|
}
|
|
6076
|
+
ensureTypeScriptForRestClient() {
|
|
6077
|
+
const hasRestClient = this.generators.has("restclient");
|
|
6078
|
+
const hasTypeScript = this.generators.has("typescript");
|
|
6079
|
+
if (hasRestClient && !hasTypeScript) {
|
|
6080
|
+
this.logger.info("Automatically adding TypeScript generator for REST client", {}, "ensureTypeScriptForRestClient");
|
|
6081
|
+
this.typescript({
|
|
6082
|
+
moduleFormat: "esm",
|
|
6083
|
+
generateIndex: true,
|
|
6084
|
+
includeDocuments: false,
|
|
6085
|
+
namingConvention: "PascalCase"
|
|
6086
|
+
});
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
3527
6089
|
async generate() {
|
|
6090
|
+
this.ensureTypeScriptForRestClient();
|
|
3528
6091
|
const startTime = performance.now();
|
|
3529
6092
|
const result = {
|
|
3530
6093
|
success: false,
|
|
@@ -3659,6 +6222,32 @@ class APIBuilder {
|
|
|
3659
6222
|
function createAPI(options) {
|
|
3660
6223
|
return new APIBuilder(options);
|
|
3661
6224
|
}
|
|
6225
|
+
function createAPIFromConfig(config) {
|
|
6226
|
+
const builder = new APIBuilder({
|
|
6227
|
+
outputDir: config.outputDir,
|
|
6228
|
+
verbose: config.verbose,
|
|
6229
|
+
overwrite: config.overwrite,
|
|
6230
|
+
validate: config.validate,
|
|
6231
|
+
cache: config.cache,
|
|
6232
|
+
typeSchemaConfig: config.typeSchema
|
|
6233
|
+
});
|
|
6234
|
+
if (config.packages && config.packages.length > 0) {
|
|
6235
|
+
for (const pkg of config.packages) {
|
|
6236
|
+
builder.fromPackage(pkg);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
if (config.files && config.files.length > 0) {
|
|
6240
|
+
builder.fromFiles(...config.files);
|
|
6241
|
+
}
|
|
6242
|
+
if (config.typescript) {
|
|
6243
|
+
builder.typescript(config.typescript);
|
|
6244
|
+
}
|
|
6245
|
+
if (config.restClient) {
|
|
6246
|
+
console.log("fsdfdsfsdfdsf");
|
|
6247
|
+
builder.restClient(config.restClient);
|
|
6248
|
+
}
|
|
6249
|
+
return builder;
|
|
6250
|
+
}
|
|
3662
6251
|
async function generateTypesFromPackage(packageName, outputDir, options = {}) {
|
|
3663
6252
|
return createAPI({
|
|
3664
6253
|
outputDir,
|
|
@@ -3683,6 +6272,24 @@ var DEFAULT_CONFIG = {
|
|
|
3683
6272
|
overwrite: true,
|
|
3684
6273
|
validate: true,
|
|
3685
6274
|
cache: true,
|
|
6275
|
+
restClient: {
|
|
6276
|
+
clientName: "FHIRClient",
|
|
6277
|
+
includeValidation: false,
|
|
6278
|
+
includeErrorHandling: true,
|
|
6279
|
+
includeRequestInterceptors: false,
|
|
6280
|
+
baseUrlOverride: "",
|
|
6281
|
+
enhancedSearch: false,
|
|
6282
|
+
chainedSearchBuilder: false,
|
|
6283
|
+
searchAutocomplete: true,
|
|
6284
|
+
generateValueSetEnums: true,
|
|
6285
|
+
includeUtilities: true,
|
|
6286
|
+
generateValidators: false,
|
|
6287
|
+
useCanonicalManager: true,
|
|
6288
|
+
defaultTimeout: 30000,
|
|
6289
|
+
defaultRetries: 0,
|
|
6290
|
+
includeDocumentation: true,
|
|
6291
|
+
generateExamples: false
|
|
6292
|
+
},
|
|
3686
6293
|
typescript: {
|
|
3687
6294
|
moduleFormat: "esm",
|
|
3688
6295
|
generateIndex: true,
|
|
@@ -3699,6 +6306,14 @@ var DEFAULT_CONFIG = {
|
|
|
3699
6306
|
fhirVersion: "R4",
|
|
3700
6307
|
resourceTypes: [],
|
|
3701
6308
|
maxDepth: 10,
|
|
6309
|
+
profileOptions: {
|
|
6310
|
+
generateKind: "interface",
|
|
6311
|
+
includeConstraints: true,
|
|
6312
|
+
includeDocumentation: true,
|
|
6313
|
+
generateValidators: true,
|
|
6314
|
+
strictMode: false,
|
|
6315
|
+
subfolder: "profiles"
|
|
6316
|
+
},
|
|
3702
6317
|
generateBuilders: false,
|
|
3703
6318
|
builderOptions: {
|
|
3704
6319
|
includeValidation: true,
|
|
@@ -3745,7 +6360,11 @@ var DEFAULT_CONFIG = {
|
|
|
3745
6360
|
validateCached: true,
|
|
3746
6361
|
forceRegenerate: false,
|
|
3747
6362
|
shareCache: true,
|
|
3748
|
-
cacheKeyPrefix: ""
|
|
6363
|
+
cacheKeyPrefix: "",
|
|
6364
|
+
profiles: {
|
|
6365
|
+
packages: [],
|
|
6366
|
+
autoDetect: true
|
|
6367
|
+
}
|
|
3749
6368
|
},
|
|
3750
6369
|
packages: [],
|
|
3751
6370
|
files: [],
|
|
@@ -3800,6 +6419,14 @@ class ConfigValidator {
|
|
|
3800
6419
|
const tsErrors = this.validateTypeScriptConfig(cfg.typescript);
|
|
3801
6420
|
result.errors.push(...tsErrors);
|
|
3802
6421
|
}
|
|
6422
|
+
if (cfg.typeSchema !== undefined) {
|
|
6423
|
+
const tsErrors = this.validateTypeSchemaConfig(cfg.typeSchema);
|
|
6424
|
+
result.errors.push(...tsErrors);
|
|
6425
|
+
}
|
|
6426
|
+
if (cfg.restClient !== undefined) {
|
|
6427
|
+
const rcErrors = this.validateRestClientConfig(cfg.restClient);
|
|
6428
|
+
result.errors.push(...rcErrors);
|
|
6429
|
+
}
|
|
3803
6430
|
if (cfg.packages !== undefined) {
|
|
3804
6431
|
if (!Array.isArray(cfg.packages)) {
|
|
3805
6432
|
result.errors.push({
|
|
@@ -3899,6 +6526,10 @@ class ConfigValidator {
|
|
|
3899
6526
|
const guardErrors = this.validateGuardOptions(cfg.guardOptions);
|
|
3900
6527
|
errors.push(...guardErrors);
|
|
3901
6528
|
}
|
|
6529
|
+
if (cfg.profileOptions !== undefined) {
|
|
6530
|
+
const profileErrors = this.validateProfileOptions(cfg.profileOptions);
|
|
6531
|
+
errors.push(...profileErrors);
|
|
6532
|
+
}
|
|
3902
6533
|
return errors;
|
|
3903
6534
|
}
|
|
3904
6535
|
validateValidatorOptions(config) {
|
|
@@ -3938,6 +6569,74 @@ class ConfigValidator {
|
|
|
3938
6569
|
}
|
|
3939
6570
|
return errors;
|
|
3940
6571
|
}
|
|
6572
|
+
validateRestClientConfig(config) {
|
|
6573
|
+
const errors = [];
|
|
6574
|
+
if (typeof config !== "object" || config === null) {
|
|
6575
|
+
errors.push({
|
|
6576
|
+
path: "restClient",
|
|
6577
|
+
message: "restClient config must be an object",
|
|
6578
|
+
value: config
|
|
6579
|
+
});
|
|
6580
|
+
return errors;
|
|
6581
|
+
}
|
|
6582
|
+
const cfg = config;
|
|
6583
|
+
if (cfg.clientName !== undefined && typeof cfg.clientName !== "string") {
|
|
6584
|
+
errors.push({
|
|
6585
|
+
path: "restClient.clientName",
|
|
6586
|
+
message: "clientName must be a string",
|
|
6587
|
+
value: cfg.clientName
|
|
6588
|
+
});
|
|
6589
|
+
}
|
|
6590
|
+
if (cfg.baseUrlOverride !== undefined && typeof cfg.baseUrlOverride !== "string") {
|
|
6591
|
+
errors.push({
|
|
6592
|
+
path: "restClient.baseUrlOverride",
|
|
6593
|
+
message: "baseUrlOverride must be a string",
|
|
6594
|
+
value: cfg.baseUrlOverride
|
|
6595
|
+
});
|
|
6596
|
+
}
|
|
6597
|
+
if (cfg.defaultTimeout !== undefined) {
|
|
6598
|
+
if (typeof cfg.defaultTimeout !== "number" || cfg.defaultTimeout <= 0) {
|
|
6599
|
+
errors.push({
|
|
6600
|
+
path: "restClient.defaultTimeout",
|
|
6601
|
+
message: "defaultTimeout must be a positive number",
|
|
6602
|
+
value: cfg.defaultTimeout
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
if (cfg.defaultRetries !== undefined) {
|
|
6607
|
+
if (typeof cfg.defaultRetries !== "number" || cfg.defaultRetries < 0) {
|
|
6608
|
+
errors.push({
|
|
6609
|
+
path: "restClient.defaultRetries",
|
|
6610
|
+
message: "defaultRetries must be a non-negative number",
|
|
6611
|
+
value: cfg.defaultRetries
|
|
6612
|
+
});
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
const booleanFields = [
|
|
6616
|
+
"includeValidation",
|
|
6617
|
+
"includeErrorHandling",
|
|
6618
|
+
"includeRequestInterceptors",
|
|
6619
|
+
"enhancedSearch",
|
|
6620
|
+
"chainedSearchBuilder",
|
|
6621
|
+
"searchAutocomplete",
|
|
6622
|
+
"generateValueSetEnums",
|
|
6623
|
+
"includeUtilities",
|
|
6624
|
+
"generateValidators",
|
|
6625
|
+
"useCanonicalManager",
|
|
6626
|
+
"includeDocumentation",
|
|
6627
|
+
"generateExamples"
|
|
6628
|
+
];
|
|
6629
|
+
for (const field of booleanFields) {
|
|
6630
|
+
if (cfg[field] !== undefined && typeof cfg[field] !== "boolean") {
|
|
6631
|
+
errors.push({
|
|
6632
|
+
path: `restClient.${field}`,
|
|
6633
|
+
message: `${field} must be a boolean`,
|
|
6634
|
+
value: cfg[field]
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
return errors;
|
|
6639
|
+
}
|
|
3941
6640
|
validateGuardOptions(config) {
|
|
3942
6641
|
const errors = [];
|
|
3943
6642
|
if (typeof config !== "object" || config === null) {
|
|
@@ -3977,6 +6676,134 @@ class ConfigValidator {
|
|
|
3977
6676
|
}
|
|
3978
6677
|
return errors;
|
|
3979
6678
|
}
|
|
6679
|
+
validateProfileOptions(config) {
|
|
6680
|
+
const errors = [];
|
|
6681
|
+
if (typeof config !== "object" || config === null) {
|
|
6682
|
+
errors.push({
|
|
6683
|
+
path: "typescript.profileOptions",
|
|
6684
|
+
message: "profileOptions must be an object",
|
|
6685
|
+
value: config
|
|
6686
|
+
});
|
|
6687
|
+
return errors;
|
|
6688
|
+
}
|
|
6689
|
+
const cfg = config;
|
|
6690
|
+
if (cfg.generateKind !== undefined) {
|
|
6691
|
+
if (!["interface", "type", "both"].includes(cfg.generateKind)) {
|
|
6692
|
+
errors.push({
|
|
6693
|
+
path: "typescript.profileOptions.generateKind",
|
|
6694
|
+
message: 'generateKind must be "interface", "type", or "both"',
|
|
6695
|
+
value: cfg.generateKind
|
|
6696
|
+
});
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
if (cfg.subfolder !== undefined && typeof cfg.subfolder !== "string") {
|
|
6700
|
+
errors.push({
|
|
6701
|
+
path: "typescript.profileOptions.subfolder",
|
|
6702
|
+
message: "subfolder must be a string",
|
|
6703
|
+
value: cfg.subfolder
|
|
6704
|
+
});
|
|
6705
|
+
}
|
|
6706
|
+
const booleanFields = [
|
|
6707
|
+
"includeConstraints",
|
|
6708
|
+
"includeDocumentation",
|
|
6709
|
+
"generateValidators",
|
|
6710
|
+
"strictMode"
|
|
6711
|
+
];
|
|
6712
|
+
for (const field of booleanFields) {
|
|
6713
|
+
if (cfg[field] !== undefined && typeof cfg[field] !== "boolean") {
|
|
6714
|
+
errors.push({
|
|
6715
|
+
path: `typescript.profileOptions.${field}`,
|
|
6716
|
+
message: `${field} must be a boolean`,
|
|
6717
|
+
value: cfg[field]
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
return errors;
|
|
6722
|
+
}
|
|
6723
|
+
validateTypeSchemaConfig(config) {
|
|
6724
|
+
const errors = [];
|
|
6725
|
+
if (typeof config !== "object" || config === null) {
|
|
6726
|
+
errors.push({
|
|
6727
|
+
path: "typeSchema",
|
|
6728
|
+
message: "typeSchema config must be an object",
|
|
6729
|
+
value: config
|
|
6730
|
+
});
|
|
6731
|
+
return errors;
|
|
6732
|
+
}
|
|
6733
|
+
const cfg = config;
|
|
6734
|
+
const booleanFields = [
|
|
6735
|
+
"enablePersistence",
|
|
6736
|
+
"validateCached",
|
|
6737
|
+
"forceRegenerate",
|
|
6738
|
+
"shareCache"
|
|
6739
|
+
];
|
|
6740
|
+
for (const field of booleanFields) {
|
|
6741
|
+
if (cfg[field] !== undefined && typeof cfg[field] !== "boolean") {
|
|
6742
|
+
errors.push({
|
|
6743
|
+
path: `typeSchema.${field}`,
|
|
6744
|
+
message: `${field} must be a boolean`,
|
|
6745
|
+
value: cfg[field]
|
|
6746
|
+
});
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
const stringFields = ["cacheDir", "cacheKeyPrefix"];
|
|
6750
|
+
for (const field of stringFields) {
|
|
6751
|
+
if (cfg[field] !== undefined && typeof cfg[field] !== "string") {
|
|
6752
|
+
errors.push({
|
|
6753
|
+
path: `typeSchema.${field}`,
|
|
6754
|
+
message: `${field} must be a string`,
|
|
6755
|
+
value: cfg[field]
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
if (cfg.maxAge !== undefined) {
|
|
6760
|
+
if (typeof cfg.maxAge !== "number" || cfg.maxAge <= 0) {
|
|
6761
|
+
errors.push({
|
|
6762
|
+
path: "typeSchema.maxAge",
|
|
6763
|
+
message: "maxAge must be a positive number",
|
|
6764
|
+
value: cfg.maxAge
|
|
6765
|
+
});
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
if (cfg.profiles !== undefined) {
|
|
6769
|
+
if (typeof cfg.profiles !== "object" || cfg.profiles === null) {
|
|
6770
|
+
errors.push({
|
|
6771
|
+
path: "typeSchema.profiles",
|
|
6772
|
+
message: "profiles must be an object",
|
|
6773
|
+
value: cfg.profiles
|
|
6774
|
+
});
|
|
6775
|
+
} else {
|
|
6776
|
+
const profiles = cfg.profiles;
|
|
6777
|
+
if (profiles.packages !== undefined) {
|
|
6778
|
+
if (!Array.isArray(profiles.packages)) {
|
|
6779
|
+
errors.push({
|
|
6780
|
+
path: "typeSchema.profiles.packages",
|
|
6781
|
+
message: "packages must be an array",
|
|
6782
|
+
value: profiles.packages
|
|
6783
|
+
});
|
|
6784
|
+
} else {
|
|
6785
|
+
profiles.packages.forEach((pkg, index) => {
|
|
6786
|
+
if (typeof pkg !== "string") {
|
|
6787
|
+
errors.push({
|
|
6788
|
+
path: `typeSchema.profiles.packages[${index}]`,
|
|
6789
|
+
message: "package name must be a string",
|
|
6790
|
+
value: pkg
|
|
6791
|
+
});
|
|
6792
|
+
}
|
|
6793
|
+
});
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
if (profiles.autoDetect !== undefined && typeof profiles.autoDetect !== "boolean") {
|
|
6797
|
+
errors.push({
|
|
6798
|
+
path: "typeSchema.profiles.autoDetect",
|
|
6799
|
+
message: "autoDetect must be a boolean",
|
|
6800
|
+
value: profiles.autoDetect
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
return errors;
|
|
6806
|
+
}
|
|
3980
6807
|
}
|
|
3981
6808
|
|
|
3982
6809
|
class ConfigLoader {
|
|
@@ -4030,6 +6857,10 @@ ${errorMessages}`);
|
|
|
4030
6857
|
typescript: {
|
|
4031
6858
|
...DEFAULT_CONFIG.typescript,
|
|
4032
6859
|
...userConfig.typescript
|
|
6860
|
+
},
|
|
6861
|
+
restClient: {
|
|
6862
|
+
...DEFAULT_CONFIG.restClient,
|
|
6863
|
+
...userConfig.restClient
|
|
4033
6864
|
}
|
|
4034
6865
|
};
|
|
4035
6866
|
}
|
|
@@ -4044,4 +6875,4 @@ function isConfig(obj) {
|
|
|
4044
6875
|
return result.valid;
|
|
4045
6876
|
}
|
|
4046
6877
|
|
|
4047
|
-
export { __toESM, __commonJS, __require, TypeSchemaCache, createLoggerFromConfig, TypeSchemaGenerator, TypeSchemaParser, TypeScriptAPIGenerator, APIBuilder, createAPI, generateTypesFromPackage, generateTypesFromFiles, DEFAULT_CONFIG, CONFIG_FILE_NAMES, ConfigValidator, ConfigLoader, configLoader, loadConfig, isConfig };
|
|
6878
|
+
export { __toESM, __commonJS, __require, TypeSchemaCache, createLoggerFromConfig, TypeSchemaGenerator, TypeSchemaParser, RestClientGenerator, TypeScriptAPIGenerator, APIBuilder, createAPI, createAPIFromConfig, generateTypesFromPackage, generateTypesFromFiles, DEFAULT_CONFIG, CONFIG_FILE_NAMES, ConfigValidator, ConfigLoader, configLoader, loadConfig, isConfig };
|