@aws/nx-plugin 1.0.0-rc.30 → 1.0.0-rc.32

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.
Files changed (59) hide show
  1. package/LICENSE-THIRD-PARTY +235 -1336
  2. package/README.md +1 -1
  3. package/package.json +1 -2
  4. package/src/mcp-server/tools/create-workspace-command.js +2 -2
  5. package/src/mcp-server/tools/create-workspace-command.js.map +1 -1
  6. package/src/open-api/ts-client/__snapshots__/generator.additional-properties.spec.ts.snap +147 -33
  7. package/src/open-api/ts-client/__snapshots__/generator.arrays.spec.ts.snap +308 -75
  8. package/src/open-api/ts-client/__snapshots__/generator.complex-types.spec.ts.snap +286 -62
  9. package/src/open-api/ts-client/__snapshots__/generator.composite-types.spec.ts.snap +674 -93
  10. package/src/open-api/ts-client/__snapshots__/generator.content-type.spec.ts.snap +211 -36
  11. package/src/open-api/ts-client/__snapshots__/generator.duplicate-types.spec.ts.snap +196 -44
  12. package/src/open-api/ts-client/__snapshots__/generator.edge-cases.spec.ts.snap +5507 -0
  13. package/src/open-api/ts-client/__snapshots__/generator.fast-api.spec.ts.snap +219 -60
  14. package/src/open-api/ts-client/__snapshots__/generator.multipart.spec.ts.snap +280 -0
  15. package/src/open-api/ts-client/__snapshots__/generator.primitive-types.spec.ts.snap +351 -80
  16. package/src/open-api/ts-client/__snapshots__/generator.request.spec.ts.snap +170 -49
  17. package/src/open-api/ts-client/__snapshots__/generator.reserved-keywords.spec.ts.snap +98 -22
  18. package/src/open-api/ts-client/__snapshots__/generator.response.spec.ts.snap +147 -33
  19. package/src/open-api/ts-client/__snapshots__/generator.streaming.spec.ts.snap +392 -88
  20. package/src/open-api/ts-client/__snapshots__/generator.tags.spec.ts.snap +196 -44
  21. package/src/open-api/ts-client/files/client.gen.ts.template +166 -19
  22. package/src/open-api/ts-client/files/types.gen.ts.template +4 -4
  23. package/src/open-api/ts-client/generator.js +1 -1
  24. package/src/open-api/ts-client/generator.js.map +1 -1
  25. package/src/open-api/ts-hooks/files/options-proxy.gen.ts.template +5 -0
  26. package/src/open-api/ts-hooks/generator.spec.tsx +70 -0
  27. package/src/open-api/utils/codegen-data/languages.d.ts +0 -6
  28. package/src/open-api/utils/codegen-data/languages.js +23 -18
  29. package/src/open-api/utils/codegen-data/languages.js.map +1 -1
  30. package/src/open-api/utils/codegen-data/types.d.ts +240 -10
  31. package/src/open-api/utils/codegen-data/types.js +24 -1
  32. package/src/open-api/utils/codegen-data/types.js.map +1 -1
  33. package/src/open-api/utils/codegen-data.d.ts +2 -2
  34. package/src/open-api/utils/codegen-data.js +357 -617
  35. package/src/open-api/utils/codegen-data.js.map +1 -1
  36. package/src/open-api/utils/normalise.js +167 -22
  37. package/src/open-api/utils/normalise.js.map +1 -1
  38. package/src/open-api/utils/parser.d.ts +55 -0
  39. package/src/open-api/utils/parser.js +754 -0
  40. package/src/open-api/utils/parser.js.map +1 -0
  41. package/src/open-api/utils/types.d.ts +18 -0
  42. package/src/open-api/utils/types.js.map +1 -1
  43. package/src/preset/__snapshots__/generator.spec.ts.snap +6 -6
  44. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +414 -6
  45. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +276 -4
  46. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +414 -6
  47. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +0 -23
  48. package/src/ts/mcp-server/generator.js +10 -22
  49. package/src/ts/mcp-server/generator.js.map +1 -1
  50. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +69 -1
  51. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +69 -1
  52. package/src/utils/mcp.js +1 -1
  53. package/src/utils/mcp.js.map +1 -1
  54. package/src/utils/names.d.ts +6 -0
  55. package/src/utils/names.js +6 -1
  56. package/src/utils/names.js.map +1 -1
  57. package/src/utils/versions.d.ts +5 -5
  58. package/src/utils/versions.js +4 -4
  59. package/src/utils/versions.js.map +1 -1
@@ -1,437 +1,352 @@
1
1
  /**
2
2
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
- */ import * as gen from "@hey-api/openapi-ts";
5
- import camelCase from "lodash.camelcase";
6
- import orderBy from "lodash.orderby";
4
+ */ import orderBy from "lodash.orderby";
7
5
  import trim from "lodash.trim";
8
6
  import uniqBy from "lodash.uniqby";
9
- import { kebabCase, pascalCase, snakeCase, toClassName, upperFirst } from "../../utils/names.js";
7
+ import { camelCase, pascalCase, snakeCase, toClassName, upperFirst } from "../../utils/names.js";
10
8
  import { toPythonName, toPythonType, toTypeScriptModelName, toTypeScriptName, toTypeScriptType } from "./codegen-data/languages.js";
11
- import { COLLECTION_TYPES, COMPOSED_SCHEMA_TYPES, flattenModelLink, PRIMITIVE_TYPES, STREAMING_CONTENT_TYPES, VENDOR_EXTENSIONS } from "./codegen-data/types.js";
9
+ import { COLLECTION_TYPES, COMPOSED_SCHEMA_TYPES, createModel, DEFAULT_SERVICE_NAME, indexModelsByName, PRIMITIVE_TYPES, STREAMING_CONTENT_TYPES, VENDOR_EXTENSIONS } from "./codegen-data/types.js";
12
10
  import { normaliseOpenApiSpecForCodeGen } from "./normalise.js";
11
+ import { buildClientData, buildInlineModel, compositeMemberSchemas, getSpecOperation, getSpecParametersByKey, getSpecPathParameters, linkModel, specParameterKey } from "./parser.js";
13
12
  import { isRef, resolveIfRef, splitRef } from "./refs.js";
14
13
  /**
15
- * Builds a data structure from an OpenAPI spec which can be used to generate code
16
- */ export const buildOpenApiCodeGenData = async (inSpec)=>{
17
- // Ensure spec is ready for codegen
14
+ * Build the data structure used to generate code from an OpenAPI spec.
15
+ */ export const buildOpenApiCodeGenData = (inSpec)=>{
18
16
  const spec = normaliseOpenApiSpecForCodeGen(inSpec);
19
- // Build the initial data, which we will augment with additional information
20
- const data = await buildInitialCodeGenData(spec);
21
- // Ensure the models have their links set when they are arrays/dictionaries
22
- ensureModelLinks(spec, data);
23
- // Mutate the models with enough data to render composite models in the templates
24
- ensureCompositeModels(data);
25
- const modelsByName = Object.fromEntries(data.models.map((m)=>[
26
- m.name,
27
- m
28
- ]));
29
- // Augment operations with additional data
17
+ const data = buildClientData(spec);
18
+ const modelsByName = indexModelsByName(data.models);
30
19
  for (const service of data.services){
31
- // Keep track of the request and response models we need the service (ie api client) to import
32
- const requestModelImports = [];
33
- const responseModelImports = [];
34
- for (const op of service.operations){
35
- // Extract the operation back from the openapi spec
36
- const specOp = spec?.paths?.[op.path]?.[op.method.toLowerCase()];
37
- op.name = op.id ?? op.name;
38
- op.uniqueName = op.name;
39
- if (specOp['x-aws-nx-deduplicated-op-id']) {
40
- op.uniqueName = specOp['x-aws-nx-deduplicated-op-id'];
41
- }
42
- if (specOp['x-aws-nx-deduplicated-dot-op-id']) {
43
- op.dotNotationName = specOp['x-aws-nx-deduplicated-dot-op-id'];
44
- }
45
- // Add vendor extensions
46
- op.vendorExtensions = op.vendorExtensions ?? {};
47
- copyVendorExtensions(specOp ?? {}, op.vendorExtensions);
48
- if (specOp) {
49
- // Add all response models to the response model imports
50
- responseModelImports.push(...op.responses.filter((r)=>r.export === 'reference').map((r)=>r.type));
51
- for (const response of op.responses){
52
- // Validate that the response is not a composite schema of primitives since we cannot determine what
53
- // the type of the response is (it all comes back as text!)
54
- if (response.export === 'reference' && COMPOSED_SCHEMA_TYPES.has(modelsByName[response.type]?.export)) {
55
- const composedPrimitives = modelsByName[response.type].composedPrimitives.filter((p)=>![
56
- 'array',
57
- 'dictionary'
58
- ].includes(p.export));
59
- if (composedPrimitives.length > 0) {
60
- throw new Error(`Operation "${op.method} ${op.path}" returns a composite schema of primitives with ${camelCase(modelsByName[response.type].export)}, which cannot be distinguished at runtime`);
61
- }
62
- }
63
- const matchingSpecResponse = specOp.responses[`${response.code}`];
64
- // @hey-api/openapi-ts does not distinguish between returning an "any" or returning "void"
65
- // We distinguish this by looking back at each response in the spec, and checking whether it
66
- // has content
67
- if (matchingSpecResponse) {
68
- // Resolve the ref if necessary
69
- const specResponse = resolveIfRef(spec, matchingSpecResponse);
70
- // When there's no content, we set the type to 'void'
71
- if (!specResponse.content) {
72
- response.type = 'void';
73
- } else {
74
- // Add the response media types
75
- const mediaTypes = Object.keys(specResponse.content);
76
- response.mediaTypes = mediaTypes;
77
- for (const mediaType of mediaTypes){
78
- const responseContent = specResponse.content?.[mediaType] ?? Object.values(specResponse.content)[0];
79
- const responseSchema = resolveIfRef(spec, responseContent.schema);
80
- if (responseSchema) {
81
- await mutateWithOpenapiSchemaProperties(spec, response, responseSchema, modelsByName);
82
- }
83
- if (STREAMING_CONTENT_TYPES.has(mediaType) && 'itemSchema' in responseContent) {
84
- response.isJsonlStreaming = true;
85
- response.itemSchemaModel = await buildOrReferenceModel(spec, modelsByName, responseContent.itemSchema);
86
- }
87
- }
88
- }
89
- }
90
- }
91
- }
92
- const specParametersByName = Object.fromEntries((specOp?.parameters ?? []).map((p)=>{
93
- const param = resolveIfRef(spec, p);
94
- return [
95
- param.name,
96
- param
97
- ];
98
- }));
99
- // Loop through the parameters
100
- for (const parameter of op.parameters){
101
- // Add the request model import
102
- if (parameter.export === 'reference') {
103
- requestModelImports.push(parameter.type);
104
- }
105
- const specParameter = specParametersByName[parameter.prop];
106
- const specParameterSchema = resolveIfRef(spec, specParameter?.schema);
107
- if (specParameterSchema) {
108
- await mutateWithOpenapiSchemaProperties(spec, parameter, specParameterSchema, modelsByName);
109
- }
110
- if (parameter.in === 'body' || parameter.in === 'formData') {
111
- // Parameter name for the body is 'body'. `formData` bodies arrive
112
- // under a synthetic `formData` param (hey-api convention) that's
113
- // logically the body — normalise both shapes to `in === 'body'`
114
- // so every downstream consumer (including ts-client's request-
115
- // type composition) sees a single body kind.
116
- parameter.name = 'body';
117
- parameter.prop = 'body';
118
- parameter.in = 'body';
119
- // The request body is not in the "parameters" section of the openapi spec so we won't have added the schema
120
- // properties above. Find it here.
121
- const specBody = resolveIfRef(spec, specOp?.requestBody);
122
- if (specBody) {
123
- if (parameter.mediaType) {
124
- const bodySchema = resolveIfRef(spec, specBody.content?.[parameter.mediaType]?.schema);
125
- if (bodySchema) {
126
- await mutateWithOpenapiSchemaProperties(spec, parameter, bodySchema, modelsByName);
127
- }
128
- }
129
- // Track all the media types that can be accepted in the request body
130
- parameter.mediaTypes = Object.keys(specBody.content);
131
- }
132
- } else if ([
133
- 'query',
134
- 'header'
135
- ].includes(parameter.in) && specParameter) {
136
- // Translate style/explode to OpenAPI v2 style collectionFormat
137
- // https://spec.openapis.org/oas/v3.0.3.html#style-values
138
- const style = specParameter.style ?? (parameter.in === 'query' ? 'form' : 'simple');
139
- const explode = specParameter.explode ?? style === 'form';
140
- if (parameter.in === 'query') {
141
- parameter.collectionFormat = explode ? 'multi' : ({
142
- spaceDelimited: 'ssv',
143
- pipeDelimited: 'tsv',
144
- simple: 'csv',
145
- form: 'csv'
146
- })[style] ?? 'multi';
147
- } else {
148
- // parameter.in === "header"
149
- parameter.collectionFormat = explode ? 'multi' : 'csv';
150
- }
151
- }
152
- mutateModelWithAdditionalTypes(parameter);
153
- }
154
- // Add language types to response models
155
- (op.responses ?? []).forEach(mutateModelWithAdditionalTypes);
156
- // Sort responses by code
157
- op.responses = orderBy(op.responses, (r)=>r.code);
158
- // Result is the lowest successful response, otherwise is the 2XX or default response
159
- const result = op.responses.find((r)=>typeof r.code === 'number' && r.code >= 200 && r.code < 300);
160
- op.result = result ?? op.responses.find((r)=>r.code === '2XX' || r.code === 'default');
161
- // Add variants of operation name
162
- op.operationIdPascalCase = pascalCase(op.uniqueName);
163
- op.operationIdKebabCase = kebabCase(op.uniqueName);
164
- op.operationIdSnakeCase = toPythonName('operation', op.uniqueName);
165
- // Add request type name (after operationIdPascalCase is set)
166
- if (op.parameters && op.parameters.length > 0) {
167
- const baseRequestTypeName = `${op.operationIdPascalCase}Request`;
168
- const hasConflict = !!spec.components?.schemas?.[baseRequestTypeName];
169
- if (hasConflict) {
170
- // Use OperationRequest suffix when there's a conflict
171
- op.requestTypeName = `${op.operationIdPascalCase}OperationRequest`;
172
- } else {
173
- // Use standard Request suffix when no conflict
174
- op.requestTypeName = baseRequestTypeName;
175
- }
176
- }
177
- mutateOperationWithAdditionalData(op);
178
- }
179
- // Lexicographical ordering of operations
180
- service.operations = orderBy(service.operations, (op)=>op.uniqueName);
181
- // Add the models to import
182
- service.modelImports = orderBy(uniqBy([
183
- ...service.imports,
184
- ...requestModelImports,
185
- ...responseModelImports
186
- ], (x)=>x));
187
- // Add the service class name
188
- service.className = `${service.name}Api`;
189
- service.classNameSnakeCase = snakeCase(service.className);
190
- service.nameSnakeCase = snakeCase(service.name);
20
+ augmentService(spec, service, modelsByName);
191
21
  }
192
- // All operations across all services
193
22
  const allOperations = uniqBy(data.services.flatMap((s)=>s.operations), (o)=>o.uniqueName);
194
- // Add additional models for operation parameters
23
+ // A model per operation request parameter position (query/path/body/...).
195
24
  data.models = [
196
25
  ...data.models,
197
- ...allOperations.flatMap((op)=>{
198
- if (op.parameters && op.parameters.length > 0) {
199
- // Build a collection of request parameter models to create based on their position (ie 'in' in the openapi spec, eg body, query, path, header, etc)
200
- const parametersByPosition = {};
201
- op.parameters.filter(// We filter out (return false for) request bodies which we "inline" - ie we don't create a property which points to the body, the request will only be the body itself
202
- (p)=>{
203
- // Always create models for non-body parameters
204
- if (p.in !== 'body') {
205
- return true;
206
- }
207
- // If the body is the only parameter, we can inline it no matter the type
208
- if (op.parameters.length === 1) {
209
- return false;
210
- }
211
- // We inline object bodies, so long as they aren't dictionaries (as dictionary keys could clash with other parameters),
212
- // and so long as they don't have a property name that clashes with another parameter
213
- const hasClashingPropertyName = (modelsByName?.[p.type]?.properties ?? []).some((prop)=>op.parameters.some((param)=>param.name === prop.name));
214
- if (p.export === 'reference' && modelsByName?.[p.type]?.export !== 'dictionary' && !hasClashingPropertyName) {
215
- return false;
216
- }
217
- // Don't inline anything else
218
- return true;
219
- }).forEach((parameter)=>{
220
- parametersByPosition[parameter.in] = [
221
- ...parametersByPosition[parameter.in] ?? [],
222
- parameter
223
- ];
224
- });
225
- // Ensure that if we have an explicit body parameter, it's called "body"
226
- const requestBodyParameter = parametersByPosition['body']?.[0];
227
- if (requestBodyParameter) {
228
- requestBodyParameter.name = 'body';
229
- requestBodyParameter.prop = 'body';
230
- }
231
- op.explicitRequestBodyParameter = requestBodyParameter;
232
- return Object.entries(parametersByPosition).map(([position, parameters])=>{
233
- const name = `${op.operationIdPascalCase}Request${upperFirst(position)}Parameters`;
234
- return {
235
- $refs: [],
236
- base: name,
237
- description: op.description,
238
- enum: null,
239
- enums: null,
240
- export: 'interface',
241
- imports: [],
242
- in: '',
243
- link: undefined,
244
- name,
245
- properties: parameters,
246
- template: '',
247
- type: name,
248
- isDefinition: false,
249
- isNullable: false,
250
- isReadOnly: false,
251
- isRequired: true
252
- };
253
- });
254
- }
255
- return [];
256
- })
26
+ ...allOperations.flatMap((op)=>buildRequestParameterModels(op, modelsByName))
257
27
  ];
258
- // Augment models with additional data
259
28
  for (const model of data.models){
260
- // Add a snake_case name
261
- model.nameSnakeCase = toPythonName('model', model.name);
262
- const matchingSpecModel = spec?.components?.schemas?.[model.name];
263
- if (matchingSpecModel) {
264
- const specModel = resolveIfRef(spec, matchingSpecModel);
265
- await mutateWithOpenapiSchemaProperties(spec, model, specModel, modelsByName);
266
- // Add unique imports
267
- model.uniqueImports = orderBy(uniqBy([
268
- ...model.imports,
269
- // Include property imports, if any
270
- ...model.properties.filter((p)=>p.export === 'reference').map((p)=>p.type)
271
- ], (x)=>x)).filter((modelImport)=>modelImport !== model.name); // Filter out self for recursive model references
272
- // Add deprecated flag if present
273
- model.deprecated = specModel.deprecated || false;
274
- // Augment properties with additional data
275
- for (const property of model.properties){
276
- const matchingSpecProperty = specModel.properties?.[property.name];
277
- if (matchingSpecProperty) {
278
- const specProperty = resolveIfRef(spec, matchingSpecProperty);
279
- await mutateWithOpenapiSchemaProperties(spec, property, specProperty, modelsByName);
280
- }
281
- }
282
- }
283
- // Augment properties with additional data
284
- model.properties.forEach((property)=>{
285
- // Add language-specific names/types
286
- mutateModelWithAdditionalTypes(property);
287
- });
29
+ augmentModel(spec, model, modelsByName);
288
30
  }
289
31
  for (const model of data.models){
290
- // Set the model's typescript name and type
291
32
  model.typescriptName = toTypeScriptModelName(model.name);
292
33
  model.typescriptType = model.typescriptName;
293
34
  }
294
- // Order models lexicographically by name
35
+ for (const model of data.models){
36
+ assertNoClashingPropertyNames(model);
37
+ }
295
38
  data.models = orderBy(data.models, (d)=>d.name);
296
- // Order services so default appears first, then otherwise by name
297
- data.services = orderBy(data.services, (s)=>s.name === 'Default' ? '' : s.name);
298
- // All operations by tags
299
- const operationsByTag = {};
300
- const untaggedOperations = [];
301
- allOperations.forEach((op)=>{
302
- const tags = op.tags;
303
- if (tags && tags.length > 0) {
304
- tags.map(camelCase).forEach((tag)=>{
305
- operationsByTag[tag] = [
306
- ...operationsByTag[tag] ?? [],
307
- op
308
- ];
309
- });
310
- } else {
311
- untaggedOperations.push(op);
312
- }
313
- });
314
- // Add top level vendor extensions
315
- const vendorExtensions = {};
316
- copyVendorExtensions(spec ?? {}, vendorExtensions);
39
+ // Default service first, then by name.
40
+ data.services = orderBy(data.services, (s)=>s.name === DEFAULT_SERVICE_NAME ? '' : s.name);
41
+ const { operationsByTag, untaggedOperations } = groupOperationsByTag(allOperations);
317
42
  return {
318
43
  ...data,
319
44
  operationsByTag,
320
45
  untaggedOperations,
321
46
  info: spec.info,
322
47
  allOperations,
323
- vendorExtensions,
48
+ vendorExtensions: vendorExtensionsOf(spec),
324
49
  className: toClassName(spec.info.title)
325
50
  };
326
51
  };
327
- const buildInitialCodeGenData = async (spec)=>{
328
- let data;
329
- // We create a plugin which will capture the client data
330
- const plugin = ()=>({
331
- name: 'plugin',
332
- // eslint-disable-next-line @typescript-eslint/no-empty-function
333
- _handler: ()=>{},
334
- _handlerLegacy: ({ client })=>{
335
- data = client;
52
+ /**
53
+ * Augment a service and each of its operations with the data needed for code
54
+ * generation (names, imports, result/request types, behavioural flags), and
55
+ * compute the set of models the service (ie API client) needs to import.
56
+ */ const augmentService = (spec, service, modelsByName)=>{
57
+ const modelImports = service.operations.flatMap((op)=>augmentOperation(spec, op, modelsByName));
58
+ service.operations = orderBy(service.operations, (op)=>op.uniqueName);
59
+ service.modelImports = orderBy(uniqBy([
60
+ ...service.imports,
61
+ ...modelImports
62
+ ], (x)=>x));
63
+ service.className = `${service.name}Api`;
64
+ service.nameSnakeCase = snakeCase(service.name);
65
+ };
66
+ /**
67
+ * Augment a single operation with all the data the templates need, returning
68
+ * the names of the models it references (for the service's import list).
69
+ */ const augmentOperation = (spec, op, modelsByName)=>{
70
+ const specOp = getSpecOperation(spec, op);
71
+ assignOperationNames(op, specOp);
72
+ op.vendorExtensions = vendorExtensionsOf(specOp);
73
+ const modelImports = [
74
+ ...specOp ? augmentResponses(spec, op, specOp, modelsByName) : [],
75
+ ...augmentParameters(spec, op, specOp, modelsByName)
76
+ ];
77
+ op.responses.forEach(addLanguageTypes);
78
+ op.responses = orderBy(op.responses, (r)=>r.code);
79
+ // Result is the lowest successful response, otherwise the 2XX or default.
80
+ op.result = op.responses.find((r)=>typeof r.code === 'number' && r.code >= 200 && r.code < 300) ?? op.responses.find((r)=>r.code === '2XX' || r.code === 'default');
81
+ op.operationIdPascalCase = pascalCase(op.uniqueName);
82
+ op.operationIdSnakeCase = toPythonName('operation', op.uniqueName);
83
+ if (op.parameters.length > 0) {
84
+ const baseRequestTypeName = `${op.operationIdPascalCase}Request`;
85
+ // Use the OperationRequest suffix when the standard Request name clashes
86
+ // with an existing schema.
87
+ op.requestTypeName = spec.components?.schemas?.[baseRequestTypeName] ? `${op.operationIdPascalCase}OperationRequest` : baseRequestTypeName;
88
+ }
89
+ augmentOperationBehaviour(op);
90
+ return modelImports;
91
+ };
92
+ /**
93
+ * Set an operation's name and its deduplicated variants from the vendor
94
+ * extensions the normaliser added.
95
+ */ const assignOperationNames = (op, specOp)=>{
96
+ const deduplicatedOpId = specOp?.['x-aws-nx-deduplicated-op-id'];
97
+ const dotNotationOpId = specOp?.['x-aws-nx-deduplicated-dot-op-id'];
98
+ op.name = op.id ?? op.name;
99
+ op.uniqueName = deduplicatedOpId ?? op.name;
100
+ if (dotNotationOpId) {
101
+ op.dotNotationName = dotNotationOpId;
102
+ }
103
+ };
104
+ /**
105
+ * Augment an operation's response models with schema-derived data, resolving
106
+ * void responses and streaming item schemas. Returns the response model names
107
+ * to import.
108
+ */ const augmentResponses = (spec, op, specOp, modelsByName)=>{
109
+ const modelImports = op.responses.filter((r)=>r.export === 'reference').map((r)=>r.type);
110
+ for (const response of op.responses){
111
+ // We cannot distinguish a composite of primitives at runtime (it all comes
112
+ // back as text), so validate this away.
113
+ if (response.export === 'reference' && COMPOSED_SCHEMA_TYPES.has(modelsByName[response.type]?.export)) {
114
+ const composedPrimitives = (modelsByName[response.type].composedPrimitives ?? []).filter((p)=>!COLLECTION_TYPES.has(p.export));
115
+ if (composedPrimitives.length > 0) {
116
+ throw new Error(`Operation "${op.method} ${op.path}" returns a composite schema of primitives with ${camelCase(modelsByName[response.type].export)}, which cannot be distinguished at runtime`);
117
+ }
118
+ }
119
+ const matchingSpecResponse = specOp.responses[`${response.code}`];
120
+ if (!matchingSpecResponse) continue;
121
+ const specResponse = resolveIfRef(spec, matchingSpecResponse);
122
+ // When there's no content, the response type is 'void'
123
+ if (!specResponse.content) {
124
+ response.type = 'void';
125
+ continue;
126
+ }
127
+ const mediaTypes = Object.keys(specResponse.content);
128
+ response.mediaTypes = mediaTypes;
129
+ for (const mediaType of mediaTypes){
130
+ const responseContent = specResponse.content[mediaType];
131
+ const responseSchema = resolveIfRef(spec, responseContent.schema);
132
+ if (responseSchema) {
133
+ augmentModelFromSchema(spec, response, responseSchema, modelsByName);
134
+ }
135
+ if (STREAMING_CONTENT_TYPES.has(mediaType) && 'itemSchema' in responseContent) {
136
+ response.isJsonlStreaming = true;
137
+ response.itemSchemaModel = buildOrReferenceModel(spec, modelsByName, responseContent.itemSchema);
336
138
  }
337
- });
338
- // Use @hey-api/openapi-ts to build the initial data structure that we'll generate clients from
339
- await gen.createClient({
340
- experimentalParser: false,
341
- input: {
342
- path: spec
343
- },
344
- output: 'unused',
345
- plugins: [
346
- plugin()
347
- ],
348
- dryRun: true,
349
- logs: {
350
- level: 'silent'
351
139
  }
352
- });
353
- if (!data) {
354
- // If this happens it indicates an update to @hey-api/openapi-ts which has removed the legacy parser
355
- throw new Error('Failed to build code generation data');
356
140
  }
357
- return data;
141
+ return modelImports;
358
142
  };
359
143
  /**
360
- * Build a model for a particular primitive schema.
361
- * Only primitives are supported since we only return one model.
362
- * For non-primitives, it assumes all referenced subschemas are already models
363
- */ const buildModelForPrimitive = async (originalSpec, schema)=>{
364
- const targetSchemaName = '___aws_nx_plugin_openapi_tmp_schema___';
365
- const spec = {
366
- openapi: '3.1.0',
367
- info: {
368
- title: 'tmp',
369
- version: '1.0.0'
370
- },
371
- paths: {},
372
- components: {
373
- schemas: {
374
- ...originalSpec.components?.schemas,
375
- [targetSchemaName]: schema
376
- }
144
+ * Augment an operation's parameter models with schema-derived data, resolving
145
+ * request bodies and query/header collection formats. Returns the parameter
146
+ * model names to import.
147
+ */ const augmentParameters = (spec, op, specOp, modelsByName)=>{
148
+ const specParametersByKey = getSpecParametersByKey(spec, specOp, getSpecPathParameters(spec, op));
149
+ const modelImports = [];
150
+ for (const parameter of op.parameters){
151
+ if (parameter.export === 'reference') {
152
+ modelImports.push(parameter.type);
153
+ }
154
+ const specParameter = specParametersByKey[specParameterKey({
155
+ in: parameter.in,
156
+ name: parameter.prop
157
+ })];
158
+ const specParameterSchema = resolveIfRef(spec, specParameter?.schema);
159
+ if (specParameterSchema) {
160
+ augmentModelFromSchema(spec, parameter, specParameterSchema, modelsByName);
377
161
  }
162
+ if (parameter.in === 'body') {
163
+ augmentBodyParameter(spec, parameter, specOp, modelsByName);
164
+ } else if ((parameter.in === 'query' || parameter.in === 'header') && specParameter) {
165
+ parameter.collectionFormat = getCollectionFormat(parameter.in, specParameter);
166
+ }
167
+ addLanguageTypes(parameter);
168
+ }
169
+ return modelImports;
170
+ };
171
+ /**
172
+ * Augment a request body parameter with its schema (the body is not in the
173
+ * spec's `parameters`, so it is resolved from `requestBody` here) and record
174
+ * its acceptable media types.
175
+ */ const augmentBodyParameter = (spec, parameter, specOp, modelsByName)=>{
176
+ // The request body parameter is named 'body' downstream.
177
+ parameter.name = 'body';
178
+ parameter.prop = 'body';
179
+ const specBody = resolveIfRef(spec, specOp?.requestBody);
180
+ if (!specBody) return;
181
+ if (parameter.mediaType) {
182
+ const bodySchema = resolveIfRef(spec, specBody.content?.[parameter.mediaType]?.schema);
183
+ if (bodySchema) {
184
+ augmentModelFromSchema(spec, parameter, bodySchema, modelsByName);
185
+ }
186
+ }
187
+ // Track all the media types that can be accepted in the request body
188
+ parameter.mediaTypes = Object.keys(specBody.content);
189
+ };
190
+ /**
191
+ * Translate an OpenAPI v3 parameter's style/explode into a v2-style
192
+ * collectionFormat used when serialising array parameters.
193
+ * @see https://spec.openapis.org/oas/v3.0.3.html#style-values
194
+ */ const getCollectionFormat = (position, specParameter)=>{
195
+ const style = specParameter.style ?? (position === 'query' ? 'form' : 'simple');
196
+ const explode = specParameter.explode ?? style === 'form';
197
+ if (position === 'header') {
198
+ return explode ? 'multi' : 'csv';
199
+ }
200
+ // `deepObject` serialises an object as `key[prop]=value` pairs regardless of
201
+ // explode; the object shape (not array collection) drives its serialisation.
202
+ if (style === 'deepObject') {
203
+ return 'deepObject';
204
+ }
205
+ return explode ? 'multi' : ({
206
+ spaceDelimited: 'ssv',
207
+ pipeDelimited: 'pipes',
208
+ simple: 'csv',
209
+ form: 'csv'
210
+ })[style] ?? 'multi';
211
+ };
212
+ /**
213
+ * Build the request parameter models for an operation — one model per parameter
214
+ * position (query/path/header/cookie/body), named e.g.
215
+ * `FooRequestQueryParameters`. Request bodies that can be represented directly
216
+ * (the sole parameter, or a non-clashing object reference) are inlined rather
217
+ * than given a wrapper model, and recorded on `op.explicitRequestBodyParameter`.
218
+ */ const buildRequestParameterModels = (op, modelsByName)=>{
219
+ if (!op.parameters || op.parameters.length === 0) {
220
+ return [];
221
+ }
222
+ // Whether the request body can be represented directly, without a wrapper
223
+ // model (ie the request will be the body itself).
224
+ const canInlineBody = (body)=>{
225
+ // If the body is the only parameter, we can inline it no matter the type
226
+ if (op.parameters.length === 1) {
227
+ return true;
228
+ }
229
+ // We inline object bodies, so long as they aren't dictionaries (as
230
+ // dictionary keys could clash with other parameters), and so long as they
231
+ // don't have a property name that clashes with another parameter
232
+ const hasClashingPropertyName = (modelsByName?.[body.type]?.properties ?? []).some((prop)=>op.parameters.some((param)=>param.name === prop.name));
233
+ return body.export === 'reference' && modelsByName?.[body.type]?.export !== 'dictionary' && !hasClashingPropertyName;
378
234
  };
379
- const data = await buildInitialCodeGenData(spec);
380
- const model = data.models.find((m)=>m.name === targetSchemaName);
381
- if (!model) {
382
- throw new Error(`Failed to construct model for schema ${JSON.stringify(schema)}`);
235
+ // Group parameters by their position (`in`: query/path/header/cookie/body),
236
+ // dropping any body that can be inlined.
237
+ const parametersByPosition = op.parameters.filter((p)=>!(p.in === 'body' && canInlineBody(p))).reduce((acc, p)=>({
238
+ ...acc,
239
+ [p.in]: [
240
+ ...acc[p.in] ?? [],
241
+ p
242
+ ]
243
+ }), {});
244
+ // The body parameter was already renamed to "body" by augmentBodyParameter.
245
+ op.explicitRequestBodyParameter = parametersByPosition['body']?.[0];
246
+ return Object.entries(parametersByPosition).map(([position, parameters])=>{
247
+ const name = `${op.operationIdPascalCase}Request${upperFirst(position)}Parameters`;
248
+ return createModel({
249
+ description: op.description,
250
+ export: 'interface',
251
+ name,
252
+ properties: parameters,
253
+ type: name,
254
+ isRequired: true
255
+ });
256
+ });
257
+ };
258
+ /**
259
+ * Augment a single model with schema-derived data (from its matching spec
260
+ * schema, if any) and language-specific names and types.
261
+ */ const augmentModel = (spec, model, modelsByName)=>{
262
+ model.nameSnakeCase = toPythonName('model', model.name);
263
+ const matchingSpecModel = spec?.components?.schemas?.[model.name];
264
+ if (matchingSpecModel) {
265
+ const specModel = resolveIfRef(spec, matchingSpecModel);
266
+ augmentModelFromSchema(spec, model, specModel, modelsByName);
267
+ for (const property of model.properties){
268
+ const matchingSpecProperty = specModel.properties?.[property.name];
269
+ if (matchingSpecProperty) {
270
+ const specProperty = resolveIfRef(spec, matchingSpecProperty);
271
+ augmentModelFromSchema(spec, property, specProperty, modelsByName);
272
+ }
273
+ }
274
+ }
275
+ model.properties.forEach(addLanguageTypes);
276
+ // Resolve the discriminator's TypeScript property name for marshalling.
277
+ if (model.discriminator) {
278
+ model.discriminator.typescriptPropertyName = toTypeScriptName(model.discriminator.propertyName);
383
279
  }
384
- ensureModelLinks(spec, data);
385
- await mutateWithOpenapiSchemaProperties(spec, model, schema, Object.fromEntries(data.models.map((m)=>[
386
- m.name,
387
- m
388
- ])));
389
- return model;
390
280
  };
391
- const buildOrReferenceModel = async (spec, modelsByName, schema)=>{
281
+ /**
282
+ * Ensure no two properties/parameters of an object model collapse onto the same
283
+ * TypeScript identifier (e.g. `foo-bar` and `foo_bar` both camelCase to
284
+ * `fooBar`, or a query and header parameter sharing a name within one request
285
+ * position). Such a clash would emit a type with duplicate members that does
286
+ * not compile, so fail fast with an actionable error instead.
287
+ */ const assertNoClashingPropertyNames = (model)=>{
288
+ const seen = new Map();
289
+ for (const property of model.properties){
290
+ // Composite members are unnamed (their names are empty); skip them.
291
+ if (!property.name) continue;
292
+ const existing = seen.get(property.typescriptName);
293
+ if (existing !== undefined && existing !== property.name) {
294
+ throw new Error(`Property name conflict in "${model.name}": "${existing}" and "${property.name}" both map to the TypeScript name "${property.typescriptName}". Please rename one of these in your OpenAPI specification.`);
295
+ }
296
+ seen.set(property.typescriptName, property.name);
297
+ }
298
+ };
299
+ /**
300
+ * Group operations by their (camelCased) tags, collecting any untagged
301
+ * operations separately.
302
+ */ const groupOperationsByTag = (allOperations)=>{
303
+ const isTagged = (op)=>!!op.tags && op.tags.length > 0;
304
+ const operationsByTag = allOperations.filter(isTagged).flatMap((op)=>op.tags.map((tag)=>[
305
+ camelCase(tag),
306
+ op
307
+ ])).reduce((acc, [tag, op])=>({
308
+ ...acc,
309
+ [tag]: [
310
+ ...acc[tag] ?? [],
311
+ op
312
+ ]
313
+ }), {});
314
+ return {
315
+ operationsByTag,
316
+ untaggedOperations: allOperations.filter((op)=>!isTagged(op))
317
+ };
318
+ };
319
+ /**
320
+ * Resolve a schema to its model: an existing named model for a `$ref`, or a
321
+ * freshly built-and-augmented model for an inline (nested value) schema.
322
+ */ const buildOrReferenceModel = (spec, modelsByName, schema)=>{
392
323
  if (isRef(schema)) {
393
324
  const name = splitRef(schema.$ref)[2];
394
325
  return modelsByName[name];
395
326
  }
396
- // Non referenced schemas won't have a model created as they are primitives and aren't covered by @heyapi
397
- // So we build the model here
398
- return await buildModelForPrimitive(spec, schema);
327
+ const model = buildInlineModel(spec, schema);
328
+ linkModel(spec, modelsByName, model, schema);
329
+ augmentModelFromSchema(spec, model, schema, modelsByName);
330
+ return model;
399
331
  };
400
332
  /**
401
- * Copy vendor extensions from the first parameter to the second
402
- */ const copyVendorExtensions = (object, vendorExtensions)=>{
403
- Object.entries(object ?? {}).forEach(([key, value])=>{
404
- if (key.startsWith('x-')) {
405
- vendorExtensions[key] = value;
406
- }
407
- });
408
- };
409
- const mutateWithOpenapiSchemaProperties = async (spec, model, schema, modelsByName, visited = new Set())=>{
333
+ * The `x-*` vendor extensions declared on an object.
334
+ */ const vendorExtensionsOf = (object)=>Object.fromEntries(Object.entries(object ?? {}).filter(([key])=>key.startsWith('x-')));
335
+ const augmentModelFromSchema = (spec, model, schema, modelsByName, visited = new Set())=>{
410
336
  model.format = schema.format;
411
- model.isInteger = schema.type === 'integer';
412
- model.isShort = schema.format === 'int32';
413
- model.isLong = schema.format === 'int64';
414
337
  model.deprecated = !!schema.deprecated;
415
338
  model.openapiType = schema.type;
416
- model.isNotSchema = !!schema.not;
417
339
  model.isEnum = !!schema.enum && schema.enum.length > 0;
418
- // Copy any schema vendor extensions
419
- model.vendorExtensions = {};
420
- copyVendorExtensions(schema, model.vendorExtensions);
421
- // Use our added vendor extension
422
- model.isHoisted = !!model.vendorExtensions?.['x-aws-nx-hoisted'];
423
- // Ensure models with additional properties are handled correctly
340
+ model.vendorExtensions = vendorExtensionsOf(schema);
341
+ // A "dictionary" has only additional properties; an "interface" may mix
342
+ // explicit and additional properties.
424
343
  if (schema.additionalProperties) {
425
- const additionalPropertiesModel = await buildOrReferenceModel(spec, modelsByName, schema.additionalProperties === true ? {} : schema.additionalProperties);
426
- // The original models treat _some_ objects with additional properties as a "dictionary", and others as an "interface"
427
- // For the purposes of our code generation, we define a "dictionary" to be a model with no explicit properties, only
428
- // additional properties. Interfaces can have a mixture of explicit properties and additional properties.
344
+ const additionalPropertiesModel = buildOrReferenceModel(spec, modelsByName, schema.additionalProperties === true ? {} : schema.additionalProperties);
429
345
  if (model.export === 'dictionary') {
430
- // Dictionaries contain a special property which is the model itself. Other properties are explicit properties.
346
+ // A dictionary carries a self-referential property; the rest are explicit
431
347
  const explicitProperties = model.properties.filter((p)=>!(p.export === 'dictionary' && p.name === model.name));
348
+ // Explicit properties make this an interface rather than a dictionary
432
349
  if (explicitProperties.length > 0 || schema.patternProperties) {
433
- // Treat this model as an interface instead of a dictionary, since this has
434
- // explicit properties
435
350
  model.export = 'interface';
436
351
  model.hasAdditionalProperties = true;
437
352
  model.additionalPropertiesModel = additionalPropertiesModel;
@@ -440,215 +355,48 @@ const mutateWithOpenapiSchemaProperties = async (spec, model, schema, modelsByNa
440
355
  } else {
441
356
  model.hasAdditionalProperties = true;
442
357
  model.additionalPropertiesModel = additionalPropertiesModel;
443
- // There's a special property named [key: string] which is the definition of the additional properties
444
- // We remove this so we don't render it as a regular property.
445
- model.properties = (model.properties ?? []).filter((p)=>p.name !== '[key: string]');
446
358
  }
447
359
  }
448
- // Ensure models with pattern properties are handled correctly
360
+ // Pattern properties can have different value types per pattern, so the model
361
+ // is an interface rather than a dictionary.
449
362
  if (schema.patternProperties) {
450
363
  const patternProperties = resolveIfRef(spec, schema.patternProperties);
451
- const patternPropertiesModels = [];
452
- // When there are pattern properties, we don't want to treat models as dictionaries since there can be more than one type of "value"
453
- // depending on what pattern the key matches
454
364
  if (model.export === 'dictionary') {
455
365
  model.export = 'interface';
456
366
  }
457
- for (const [pattern, patternProperty] of Object.entries(patternProperties)){
458
- const patternPropertyModel = await buildOrReferenceModel(spec, modelsByName, patternProperty);
459
- if (patternPropertyModel) {
460
- patternPropertiesModels.push({
461
- pattern,
462
- model: patternPropertyModel
463
- });
464
- }
465
- }
466
367
  model.hasPatternProperties = true;
467
- model.patternPropertiesModels = patternPropertiesModels;
368
+ model.patternPropertiesModels = Object.entries(patternProperties).map(([pattern, patternProperty])=>({
369
+ pattern,
370
+ model: buildOrReferenceModel(spec, modelsByName, patternProperty)
371
+ })).filter((entry)=>!!entry.model);
468
372
  }
469
- mutateModelWithAdditionalTypes(model);
373
+ addLanguageTypes(model);
470
374
  visited.add(model);
471
- const modelLink = flattenModelLink(model.link);
472
- // Also apply to array items recursively
473
- if (model.export === 'array' && modelLink && 'items' in schema && schema.items && !visited.has(modelLink)) {
474
- const subSchema = resolveIfRef(spec, schema.items);
475
- await mutateWithOpenapiSchemaProperties(spec, modelLink, subSchema, modelsByName, visited);
375
+ const recurse = (target, subSchema)=>augmentModelFromSchema(spec, target, subSchema, modelsByName, visited);
376
+ // Array element type.
377
+ if (model.export === 'array' && model.link && 'items' in schema && schema.items && !visited.has(model.link)) {
378
+ recurse(model.link, resolveIfRef(spec, schema.items));
476
379
  }
477
- // Also apply to object properties recursively
478
- if (model.export === 'dictionary' && model.link && 'additionalProperties' in schema && schema.additionalProperties && !visited.has(modelLink)) {
380
+ // Dictionary value type (additionalProperties may be `true` rather than a
381
+ // schema).
382
+ if (model.export === 'dictionary' && model.link && 'additionalProperties' in schema && schema.additionalProperties && !visited.has(model.link)) {
479
383
  const subSchema = resolveIfRef(spec, schema.additionalProperties);
480
- // Additional properties can be "true" rather than a type
481
384
  if (subSchema !== true) {
482
- await mutateWithOpenapiSchemaProperties(spec, modelLink, subSchema, modelsByName, visited);
483
- }
484
- }
485
- for (const property of model.properties.filter((p)=>!visited.has(p) && schema.properties?.[trim(p.name, `"'`)])){
486
- const subSchema = resolveIfRef(spec, schema.properties[trim(property.name, `"'`)]);
487
- await mutateWithOpenapiSchemaProperties(spec, property, subSchema, modelsByName, visited);
488
- }
489
- if (COMPOSED_SCHEMA_TYPES.has(model.export)) {
490
- for(let i = 0; i < model.properties.length; i++){
491
- const subSchema = resolveIfRef(spec, schema[camelCase(model.export)]?.[i]);
492
- if (subSchema) {
493
- await mutateWithOpenapiSchemaProperties(spec, model.properties[i], subSchema, modelsByName, visited);
494
- }
495
- }
496
- }
497
- };
498
- /**
499
- * Ensure that the "link" property of all dictionary/array models and properties are set recursively
500
- */ const ensureModelLinks = (spec, data)=>{
501
- const modelsByName = Object.fromEntries(data.models.map((m)=>[
502
- m.name,
503
- m
504
- ]));
505
- const visited = new Set();
506
- // Ensure set for all models
507
- data.models.forEach((model)=>{
508
- const schema = resolveIfRef(spec, spec?.components?.schemas?.[model.name]);
509
- if (schema) {
510
- // Object schemas should be typed as the model we will create
511
- if (schema.type === 'object' && (schema.properties || schema.patternProperties)) {
512
- model.type = model.name;
513
- }
514
- _ensureModelLinks(spec, modelsByName, model, schema, visited);
515
- }
516
- });
517
- // Ensure set for all parameters and responses
518
- data.services.forEach((service)=>{
519
- service.operations.forEach((op)=>{
520
- const specOp = spec?.paths?.[op.path]?.[op.method.toLowerCase()];
521
- const specParametersByName = Object.fromEntries((specOp?.parameters ?? []).map((p)=>{
522
- const param = resolveIfRef(spec, p);
523
- return [
524
- param.name,
525
- param
526
- ];
527
- }));
528
- op.parameters.forEach((parameter)=>{
529
- const specParameter = specParametersByName[parameter.prop];
530
- const specParameterSchema = resolveIfRef(spec, specParameter?.schema);
531
- if (specParameterSchema) {
532
- _ensureModelLinks(spec, modelsByName, parameter, specParameterSchema, visited);
533
- } else if (parameter.in === 'body') {
534
- // Body is not in the "parameters" section of the OpenAPI spec so we handle it in an explicit case here
535
- const specBody = resolveIfRef(spec, specOp?.requestBody);
536
- const specBodySchema = resolveIfRef(spec, specBody?.content?.[parameter.mediaType]?.schema);
537
- if (specBodySchema) {
538
- _ensureModelLinks(spec, modelsByName, parameter, specBodySchema, visited);
539
- }
540
- }
541
- });
542
- op.responses.forEach((response)=>{
543
- const specResponse = resolveIfRef(spec, specOp?.responses?.[response.code]);
544
- const mediaTypes = Object.keys(specResponse?.content ?? {});
545
- mediaTypes.forEach((mediaType)=>{
546
- const responseSchema = resolveIfRef(spec, specResponse?.content?.[mediaType]?.schema);
547
- if (responseSchema) {
548
- _ensureModelLinks(spec, modelsByName, response, responseSchema, visited);
549
- }
550
- });
551
- });
552
- });
553
- });
554
- };
555
- const _ensureModelLinks = (spec, modelsByName, model, schema, visited)=>{
556
- if (visited.has(model)) {
557
- return;
558
- }
559
- visited.add(model);
560
- if (model.export === 'dictionary' && 'additionalProperties' in schema && schema.additionalProperties) {
561
- if (isRef(schema.additionalProperties)) {
562
- const name = splitRef(schema.additionalProperties.$ref)[2];
563
- if (modelsByName[name] && !model.link) {
564
- model.link = modelsByName[name];
565
- }
566
- } else if (model.link && typeof schema.additionalProperties !== 'boolean') {
567
- _ensureModelLinks(spec, modelsByName, flattenModelLink(model.link), schema.additionalProperties, visited);
568
- }
569
- } else if (model.export === 'array' && 'items' in schema && schema.items) {
570
- if (isRef(schema.items)) {
571
- const name = splitRef(schema.items.$ref)[2];
572
- if (modelsByName[name] && !model.link) {
573
- model.link = modelsByName[name];
574
- }
575
- } else if (model.link) {
576
- _ensureModelLinks(spec, modelsByName, flattenModelLink(model.link), schema.items, visited);
385
+ recurse(model.link, subSchema);
577
386
  }
578
387
  }
579
- model.properties.filter((p)=>!visited.has(p) && schema.properties?.[trim(p.name, `"'`)]).forEach((property)=>{
580
- const subSchema = resolveIfRef(spec, schema.properties[trim(property.name, `"'`)]);
581
- _ensureModelLinks(spec, modelsByName, property, subSchema, visited);
582
- });
388
+ model.properties.filter((p)=>!visited.has(p) && schema.properties?.[trim(p.name, `"'`)]).forEach((property)=>recurse(property, resolveIfRef(spec, schema.properties[trim(property.name, `"'`)])));
583
389
  if (COMPOSED_SCHEMA_TYPES.has(model.export)) {
390
+ const memberSchemas = compositeMemberSchemas(schema, model.export);
584
391
  model.properties.forEach((property, i)=>{
585
- const subSchema = resolveIfRef(spec, schema[camelCase(model.export)]?.[i]);
392
+ const subSchema = resolveIfRef(spec, memberSchemas[i]);
586
393
  if (subSchema) {
587
- _ensureModelLinks(spec, modelsByName, property, subSchema, visited);
394
+ recurse(property, subSchema);
588
395
  }
589
396
  });
590
397
  }
591
398
  };
592
- /**
593
- * Mutates the given data to ensure composite models (ie allOf, oneOf, anyOf) have the necessary
594
- * properties for representing them in generated code. Adds `composedModels` and `composedPrimitives`
595
- * which contain the models and primitive types that each model is composed of.
596
- */ const ensureCompositeModels = (data)=>{
597
- const visited = new Set();
598
- data.models.forEach((model)=>mutateModelWithCompositeProperties(data, model, visited));
599
- };
600
- const mutateModelWithCompositeProperties = (data, model, visited)=>{
601
- if (COMPOSED_SCHEMA_TYPES.has(model.export) && !visited.has(model)) {
602
- visited.add(model);
603
- // Find the models/primitives which this is composed from
604
- const composedModelReferences = model.properties.filter((p)=>!p.name && p.export === 'reference');
605
- const composedPrimitives = model.properties.filter((p)=>!p.name && p.export !== 'reference');
606
- const modelsByName = Object.fromEntries(data.models.map((m)=>[
607
- m.name,
608
- m
609
- ]));
610
- let composedModels = composedModelReferences.flatMap((r)=>modelsByName[r.type] ? [
611
- modelsByName[r.type]
612
- ] : []);
613
- // Recursively resolve composed properties of properties, to ensure mixins for all-of include all recursive all-of properties
614
- composedModels.forEach((m)=>mutateModelWithCompositeProperties(data, m, visited));
615
- // Enums are models, however they are serialised as primitives and so should be moved to the primitives list
616
- composedPrimitives.push(...composedModels.filter((m)=>m.export === 'enum'));
617
- composedModels = composedModels.filter((m)=>m.export !== 'enum');
618
- // When multiple arrays of non-primitives are composed using allOf/oneOf/anyOf, it's not possible to distinguish at runtime which
619
- // type it is, and so we validate this away.
620
- // TODO: consider honouring more advanced OpenAPI spec features like "discriminators" which can help for this case, but in practice
621
- // users are unlikely to model their API this way
622
- const isPrimitiveArray = (m)=>{
623
- if (m.link && [
624
- 'array',
625
- 'dictionary'
626
- ].includes(m.export)) {
627
- return isPrimitiveArray(flattenModelLink(m.link));
628
- }
629
- return PRIMITIVE_TYPES.has(m.type) && ![
630
- 'date',
631
- 'date-time'
632
- ].includes(m.format);
633
- };
634
- const arrayComposedModels = composedPrimitives.filter((m)=>m.export === 'array' && !isPrimitiveArray(m));
635
- if (arrayComposedModels.length > 1) {
636
- throw new Error(`Schema "${model.name}" defines ${camelCase(model.export)} with multiple array types which cannot be distinguished at runtime.`);
637
- }
638
- // For all-of models, we include all composed model properties.
639
- if (model.export === 'all-of') {
640
- if (composedPrimitives.length > 0) {
641
- throw new Error(`Schema "${model.name}" defines allOf with non-object types. allOf may only compose object types in the OpenAPI specification.`);
642
- }
643
- }
644
- model.composedModels = composedModels;
645
- model.composedPrimitives = composedPrimitives;
646
- }
647
- };
648
- /**
649
- * Mutates the given model to add language specific types and names
650
- */ const mutateModelWithAdditionalTypes = (model)=>{
651
- // Trim any surrounding quotes from name
399
+ const addLanguageTypes = (model)=>{
652
400
  model.name = trim(model.name, `"'`);
653
401
  model.typescriptName = toTypeScriptName(model.name);
654
402
  model.typescriptType = toTypeScriptType(model);
@@ -656,17 +404,14 @@ const mutateModelWithCompositeProperties = (data, model, visited)=>{
656
404
  model.pythonType = toPythonType(model);
657
405
  model.isPrimitive = PRIMITIVE_TYPES.has(model.type) && !COMPOSED_SCHEMA_TYPES.has(model.export) && !COLLECTION_TYPES.has(model.export);
658
406
  };
659
- /**
660
- * Determine whether or not an operation is a mutation
661
- */ const isOperationMutation = (op)=>{
662
- // Let the user override whether an operation is a query or mutation using x-mutation/x-query
407
+ const isOperationMutation = (op)=>{
408
+ // x-mutation/x-query override the HTTP-method default.
663
409
  const { vendorExtensions } = op;
664
410
  if (vendorExtensions?.[VENDOR_EXTENSIONS.MUTATION]) {
665
411
  return true;
666
412
  } else if (vendorExtensions?.[VENDOR_EXTENSIONS.QUERY]) {
667
413
  return false;
668
414
  }
669
- // Assume a restful API and treat mutative HTTP methods as mutations
670
415
  return [
671
416
  'PATCH',
672
417
  'POST',
@@ -674,80 +419,75 @@ const mutateModelWithCompositeProperties = (data, model, visited)=>{
674
419
  'DELETE'
675
420
  ].includes(op.method);
676
421
  };
677
- /**
678
- * Add infinite query details to the operation
679
- */ const mutateOperationWithInfiniteQueryDetails = (op)=>{
422
+ const augmentInfiniteQuery = (op)=>{
680
423
  const { paginationDisabled, cursorPropertyName } = getCursorOptions(op);
681
- // Allow users to customise the "cursor" property used for paginated requests
682
424
  const cursorProperty = op.parameters.find((p)=>p.name === cursorPropertyName);
683
- // The operation is an infinite query if:
684
- // - x-cursor is not set to 'false' (this allows users to disable infinite queries for operations that accept a 'cursor')
685
- // - the operation accepts a parameter named 'cursor', or a parameter named as the user specified with x-cursor
425
+ // An infinite query is a paginated operation that accepts the cursor parameter
686
426
  op.isInfiniteQuery = !paginationDisabled && !!cursorProperty;
687
427
  if (op.isInfiniteQuery) {
688
428
  op.infiniteQueryCursorProperty = cursorProperty;
689
429
  }
690
430
  };
691
431
  /**
692
- * Extracts the cursor options for this operation based on the vendor extensions
693
- *
694
- * Valid usage of x-cursor:
695
- *
696
- * x-cursor: 'property' - customises the input property used for pagination
697
- * x-cursor: false - disables pagination for the operation (ie the operation would have pagination by default as an input 'cursor' property exists)
698
- *
699
- * We also support object equivalents of the above, since Smithy vendor extensions can only be objects
432
+ * Resolve the pagination cursor options from an operation's `x-cursor` vendor
433
+ * extension. Accepted forms (object variants exist because Smithy vendor
434
+ * extensions must be objects):
700
435
  *
701
- * x-cursor: { inputToken: 'property' }
702
- * x-cursor: { enabled: false }
436
+ * - `'property'` / `{ inputToken: 'property' }` — the input property to page on
437
+ * - `false` / `{ enabled: false }` — disable pagination
703
438
  */ const getCursorOptions = (op)=>{
704
- const { vendorExtensions } = op;
705
- const cursorExtension = vendorExtensions?.[VENDOR_EXTENSIONS.CURSOR];
706
- // By default pagination isn't explicitly disabled and the cursor property to look for is 'cursor'
707
- let paginationDisabled = false;
708
- let cursorPropertyName = 'cursor';
709
- if (typeof cursorExtension === 'boolean' && cursorExtension === false) {
710
- paginationDisabled = true;
711
- } else if (typeof cursorExtension === 'string') {
712
- cursorPropertyName = cursorExtension;
713
- } else if (typeof cursorExtension === 'object') {
714
- if (cursorExtension?.enabled === false) {
715
- paginationDisabled = true;
716
- }
717
- if (cursorExtension?.inputToken && typeof cursorExtension.inputToken === 'string') {
718
- cursorPropertyName = cursorExtension.inputToken;
719
- }
439
+ const cursor = op.vendorExtensions?.[VENDOR_EXTENSIONS.CURSOR];
440
+ // Defaults: pagination enabled, paging on a property named 'cursor'.
441
+ if (cursor === false) {
442
+ return {
443
+ paginationDisabled: true,
444
+ cursorPropertyName: 'cursor'
445
+ };
720
446
  }
721
- // x-cursor can be: a string (indicating pagination is enabled and the property name to use)
447
+ if (typeof cursor === 'string') {
448
+ return {
449
+ paginationDisabled: false,
450
+ cursorPropertyName: cursor
451
+ };
452
+ }
453
+ const options = cursor ?? {};
722
454
  return {
723
- paginationDisabled,
724
- cursorPropertyName
455
+ paginationDisabled: options.enabled === false,
456
+ cursorPropertyName: typeof options.inputToken === 'string' ? options.inputToken : 'cursor'
725
457
  };
726
458
  };
727
459
  /**
728
- * Add additional data to an operation for code generation decisions
729
- */ const mutateOperationWithAdditionalData = (op)=>{
730
- // Add mutation/query details
460
+ * Add query/mutation, streaming and infinite-query flags to an operation.
461
+ */ const augmentOperationBehaviour = (op)=>{
731
462
  const isMutation = isOperationMutation(op);
732
463
  op.isMutation = isMutation;
733
464
  op.isQuery = !isMutation;
734
- // Streaming responses
735
465
  op.isStreaming = !!op.vendorExtensions?.[VENDOR_EXTENSIONS.STREAMING] || op.responses.some((res)=>res.isJsonlStreaming);
736
- // Set the result type for the client method to the item schema so that
737
- // it will resolve to AsyncIterableIterator<{itemSchemaModel.name}>
466
+ // For JSON-lines streaming, the result is each streamed item, so the client
467
+ // method returns AsyncIterableIterator<itemSchemaModel>. Named (hoisted)
468
+ // item schemas become references; inline primitives keep their own type.
738
469
  const jsonlResponse = op.responses.find((res)=>res.isJsonlStreaming);
739
470
  if (jsonlResponse) {
740
471
  const itemSchemaModel = jsonlResponse.itemSchemaModel;
741
472
  const result = op.result;
742
473
  if (result && itemSchemaModel) {
743
- result.type = itemSchemaModel.name;
744
- result.typescriptType = itemSchemaModel.name;
745
- result.export = 'reference';
474
+ if (itemSchemaModel.name) {
475
+ result.type = itemSchemaModel.name;
476
+ result.typescriptType = itemSchemaModel.name;
477
+ result.export = 'reference';
478
+ } else {
479
+ result.type = itemSchemaModel.type;
480
+ result.typescriptType = itemSchemaModel.typescriptType;
481
+ result.export = itemSchemaModel.export;
482
+ result.format = itemSchemaModel.format;
483
+ result.link = itemSchemaModel.link;
484
+ result.isPrimitive = itemSchemaModel.isPrimitive;
485
+ }
746
486
  }
747
487
  }
748
488
  // Add infinite query details if applicable
749
489
  if (!isMutation) {
750
- mutateOperationWithInfiniteQueryDetails(op);
490
+ augmentInfiniteQuery(op);
751
491
  }
752
492
  };
753
493