@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.
@@ -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 (fhirSchema.url) {
1795
- if (fhirSchema.url.includes("/us/core/")) {
1796
- metadata.package = "hl7.fhir.us.core";
1797
- } else if (fhirSchema.url.includes("hl7.org/fhir/")) {
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.rules,
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/typescript.ts
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/utils.ts
2959
- function toPascalCase(input) {
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
- currentSchemaName;
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.options = {
2998
- moduleFormat: "esm",
2999
- generateIndex: true,
3000
- includeDocuments: true,
3001
- namingConvention: "PascalCase",
3002
- includeExtensions: false,
3003
- includeProfiles: false,
3004
- ...options
3005
- };
3006
- }
3007
- async transformSchema(schema) {
3008
- this.currentSchemaName = this.formatTypeName(schema.identifier.name);
3009
- if (schema.identifier.kind === "value-set" || schema.identifier.kind === "binding" || schema.identifier.kind === "primitive-type" || schema.identifier.kind === "profile") {
3010
- return;
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
- this.imports.clear();
3013
- this.exports.clear();
3014
- this.currentSchemaName = this.formatTypeName(schema.identifier.name);
3015
- const content = this.generateTypeScriptForSchema(schema);
3016
- const imports = new Map(this.imports);
3017
- const filename = this.getFilename(schema.identifier);
3018
- const exports = Array.from(this.exports.keys());
3019
- return {
3020
- content,
3021
- imports,
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
- generateTypeScriptForSchema(schema) {
3027
- const lines = [];
3028
- const interfaceName = this.formatTypeName(schema.identifier.name);
3029
- let overridedName;
3030
- if (interfaceName === "Reference") {
3031
- overridedName = `Reference<T extends ResourceTypes = ResourceTypes>`;
3032
- }
3033
- this.exports.add(interfaceName);
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
- if (schema.identifier.kind === "resource" && interfaceName !== "DomainResource" && interfaceName !== "Resource") {
3042
- this.resourceTypes.add(interfaceName);
3043
- lines.push(` resourceType: '${interfaceName}';`);
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
- if (isTypeSchemaForResourceComplexTypeLogical(schema)) {
3046
- if (schema.fields) {
3047
- for (const [fieldName, field] of Object.entries(schema.fields)) {
3048
- const fieldLine = this.generateField(fieldName, field, {
3049
- isNested: "type" in field && field.type.kind === "nested",
3050
- baseName: interfaceName
3051
- });
3052
- if (fieldLine) {
3053
- lines.push(` ${fieldLine}`);
3054
- }
3055
- }
3056
- }
3057
- lines.push("}");
3058
- if (schema.nested) {
3059
- for (const nested of schema.nested) {
3060
- lines.push("");
3061
- lines.push(this.generateNested(this.currentSchemaName ?? "", nested));
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
- return lines.join(`
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
- generateNested(baseName, nested) {
3071
- const lines = [];
3072
- const interfaceName = this.formatTypeName(nested.identifier.name);
3073
- this.exports.add(interfaceName);
3074
- if (nested.base) {
3075
- const baseInterface = this.getType(nested.base);
3076
- if (baseInterface.isPrimitive) {
3077
- lines.push(`export interface ${baseName}${interfaceName}{`);
3078
- } else {
3079
- this.imports.set(baseInterface.value, baseInterface.value);
3080
- lines.push(`export interface ${baseName}${interfaceName} extends ${baseInterface.value} {`);
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
- if (nested.fields) {
3086
- for (const [fieldName, field] of Object.entries(nested.fields)) {
3087
- const fieldLine = this.generateField(fieldName, field);
3088
- if (fieldLine) {
3089
- lines.push(` ${fieldLine}`);
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
- generateField(fieldName, field, nestedOpts) {
3098
- if (isPolymorphicInstanceField(field)) {
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
- generatePolymorphicInstance(fieldName, field, nestedOpts) {
3106
- let typeString = "any";
3107
- if (field.reference) {
3108
- typeString = this.buildReferenceType(field.reference);
3109
- } else if (field.type) {
3110
- const subType = this.getType(field.type);
3111
- if (!subType.isPrimitive && !nestedOpts?.isNested) {
3112
- this.imports.set(subType.value, subType.value);
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
- generateRegularField(fieldName, field, nestedOpts) {
3121
- let typeString = "any";
3122
- if (field.enum) {
3123
- if (field.enum.length > 15) {
3124
- typeString = "string";
3125
- } else {
3126
- typeString = `${field.enum.map((e) => `'${e}'`).join(" | ")}`;
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
- } else if (field.reference) {
3129
- typeString = this.buildReferenceType(field.reference);
3130
- } else if (field.type) {
3131
- const subType = this.getType(field.type);
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
- if (nestedOpts?.baseName === "Reference" && fieldName === "type") {
3138
- typeString = "T";
3139
- this.imports.set("ResourceTypes", "utility");
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
- buildReferenceType(refers) {
3146
- this.imports.set("Reference", "Reference");
3147
- if (refers.length === 0) {
3148
- return "Reference";
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
- if (refers.length === 1 && refers[0]?.name === "Resource") {
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 mkdir2(this.options.outputDir, { recursive: true });
5307
+ await mkdir3(this.options.outputDir, { recursive: true });
3170
5308
  if (this.options.includeProfiles) {
3171
- await mkdir2(join10(this.options.outputDir, "profiles"), {
5309
+ await mkdir3(join11(this.options.outputDir, "profiles"), {
3172
5310
  recursive: true
3173
5311
  });
3174
5312
  }
3175
5313
  if (this.options.includeExtensions) {
3176
- await mkdir2(join10(this.options.outputDir, "extensions"), {
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: `export type ResourceTypes = ${Array.from(this.resourceTypes.keys().map((key) => `'${key}'`)).join(" | ")};
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 = join10(this.options.outputDir, result.filename);
5345
+ const filePath = join11(this.options.outputDir, result.filename);
3190
5346
  await this.ensureDirectoryExists(filePath);
3191
- await writeFile4(filePath, `${this.generateImportStatements(result.imports)}
5347
+ const imports = result.filename === "utility.ts" ? "" : this.generateImportStatements(result.imports);
5348
+ const content = imports ? `${imports}
3192
5349
 
3193
- ${result.content}`, "utf-8");
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 = join10(this.options.outputDir, "index.ts");
3204
- await writeFile4(indexPath, indexFile.content, "utf-8");
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
- for (const [item, pkg] of imports.entries()) {
3218
- lines.push(`import type { ${item} } from './${pkg}';`);
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: join10(this.options.outputDir, result.filename),
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: join10(this.options.outputDir, "index.ts"),
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("primitive")) {
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("profile")) {
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 (profiles.length > 0) {
3344
- lines.push("// Profiles namespace - contains all FHIR Profile types");
3345
- lines.push('export * as Profiles from "./profiles";');
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
- const content = lines.join(`
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
- filterSchemas(schemas) {
3357
- if (this.options.includeExtensions) {
3358
- return schemas;
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
- return schemas;
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
- for (const subfolder of subfolders) {
3368
- const subfolderResults = results.filter((r) => r.filename.startsWith(`${subfolder}/`));
3369
- const indexContent = this.generateSubfolderIndex(subfolderResults, subfolder);
3370
- const indexPath = join10(this.options.outputDir, subfolder, "index.ts");
3371
- await writeFile4(indexPath, indexContent, "utf-8");
3372
- generatedFiles.push({
3373
- path: indexPath,
3374
- filename: `${subfolder}/index.ts`,
3375
- content: indexContent,
3376
- exports: subfolderResults.flatMap((r) => r.exports)
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
- generateSubfolderIndex(results, subfolder) {
5890
+ generateEnumTypes() {
3381
5891
  const lines = [];
3382
- lines.push("/**");
3383
- lines.push(` * ${subfolder.charAt(0).toUpperCase() + subfolder.slice(1)} Index`);
3384
- lines.push(" * ");
3385
- lines.push(" * Auto-generated exports for all types in this subfolder.");
3386
- lines.push(" */");
3387
- lines.push("");
3388
- if (results.length === 0) {
3389
- lines.push("// No types in this category");
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
- formatTypeName(name) {
3414
- if (this.options.namingConvention === "PascalCase") {
3415
- return toPascalCase(name);
3416
- }
3417
- return name;
5909
+ toPascalCase(str) {
5910
+ return str.charAt(0).toUpperCase() + str.slice(1);
3418
5911
  }
3419
- getType(identifier) {
3420
- const primitiveType = PRIMITIVE_TYPE_MAP[identifier.name];
3421
- if (primitiveType) {
3422
- return { isPrimitive: true, value: primitiveType };
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
- return { isPrimitive: false, value: this.formatTypeName(identifier.name) };
3425
- }
3426
- getBaseInterface(schema) {
3427
- if (isTypeSchemaForResourceComplexTypeLogical(schema) && schema.base) {
3428
- return this.getType(schema.base);
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
- return null;
3431
- }
3432
- getFilename(identifier) {
3433
- const name = toPascalCase(identifier.name);
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 };