@aws/nx-plugin 1.0.0-rc.31 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.31",
3
+ "version": "1.0.0-rc.32",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -1,7 +1,10 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`openApiTsClientGenerator - content type header > should not force Content-Type for multipart/form-data bodies > client.gen.ts 1`] = `
4
- "import type { PostUploadRequest } from './types.gen.js';
4
+ "import type {
5
+ PostUploadRequestContent,
6
+ PostUploadRequest,
7
+ } from './types.gen.js';
5
8
 
6
9
  /**
7
10
  * Utility for serialisation and deserialisation of API types.
@@ -9,6 +12,39 @@ exports[`openApiTsClientGenerator - content type header > should not force Conte
9
12
  export class $IO {
10
13
  public static $mapValues = (data: any, fn: (item: any) => any) =>
11
14
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
15
+
16
+ public static PostUploadRequestContent = {
17
+ toJson: (model: PostUploadRequestContent): any => {
18
+ if (model === undefined || model === null) {
19
+ return model;
20
+ }
21
+ return {
22
+ ...(model.file === undefined
23
+ ? {}
24
+ : {
25
+ file: model.file,
26
+ }),
27
+ ...(model.description === undefined
28
+ ? {}
29
+ : {
30
+ description: model.description,
31
+ }),
32
+ };
33
+ },
34
+ fromJson: (json: any): PostUploadRequestContent => {
35
+ if (json === undefined || json === null) {
36
+ return json;
37
+ }
38
+ return {
39
+ file: json['file'],
40
+ ...(json['description'] === undefined
41
+ ? {}
42
+ : {
43
+ description: json['description'],
44
+ }),
45
+ };
46
+ },
47
+ };
12
48
  }
13
49
 
14
50
  /**
@@ -130,6 +166,26 @@ export class TestApi {
130
166
  });
131
167
  };
132
168
 
169
+ private $formData = (model: { [key: string]: any }): FormData => {
170
+ const formData = new FormData();
171
+ const append = (key: string, value: any): void => {
172
+ if (value === undefined || value === null) {
173
+ return;
174
+ }
175
+ if (value instanceof Blob || typeof value === 'string') {
176
+ formData.append(key, value);
177
+ } else if (Array.isArray(value)) {
178
+ value.forEach((v) => append(key, v));
179
+ } else if (typeof value === 'object') {
180
+ formData.append(key, JSON.stringify(value));
181
+ } else {
182
+ formData.append(key, String(value));
183
+ }
184
+ };
185
+ Object.entries(model).forEach(([key, value]) => append(key, value));
186
+ return formData;
187
+ };
188
+
133
189
  private $fetch: typeof fetch = (...args) =>
134
190
  (this.$config.fetch ?? fetch)(...args);
135
191
 
@@ -137,7 +193,7 @@ export class TestApi {
137
193
  const pathParameters: { [key: string]: any } = {};
138
194
  const queryParameters: { [key: string]: any } = {};
139
195
  const headerParameters: { [key: string]: any } = {};
140
- const body = input as any;
196
+ const body = this.$formData($IO.PostUploadRequestContent.toJson(input));
141
197
 
142
198
  const response = await this.$fetch(
143
199
  this.$url('/upload', pathParameters, queryParameters),
@@ -160,7 +216,12 @@ export class TestApi {
160
216
  `;
161
217
 
162
218
  exports[`openApiTsClientGenerator - content type header > should not force Content-Type for multipart/form-data bodies > types.gen.ts 1`] = `
163
- "export type PostUploadRequest = unknown;
219
+ "export type PostUploadRequestContent = {
220
+ file: Blob;
221
+ description?: string;
222
+ };
223
+
224
+ export type PostUploadRequest = PostUploadRequestContent;
164
225
  export type PostUploadError = never;
165
226
  "
166
227
  `;
@@ -0,0 +1,280 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`openApiTsClientGenerator - multipart/form-data > should match the generated client snapshot > client.gen.ts 1`] = `
4
+ "import type {
5
+ Upload200Response,
6
+ UploadRequestContent,
7
+ UploadRequest,
8
+ } from './types.gen.js';
9
+
10
+ /**
11
+ * Utility for serialisation and deserialisation of API types.
12
+ */
13
+ export class $IO {
14
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
15
+ Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
16
+
17
+ public static Upload200Response = {
18
+ toJson: (model: Upload200Response): any => {
19
+ if (model === undefined || model === null) {
20
+ return model;
21
+ }
22
+ return {
23
+ ...(model.size === undefined
24
+ ? {}
25
+ : {
26
+ size: model.size,
27
+ }),
28
+ ...(model.note === undefined
29
+ ? {}
30
+ : {
31
+ note: model.note,
32
+ }),
33
+ };
34
+ },
35
+ fromJson: (json: any): Upload200Response => {
36
+ if (json === undefined || json === null) {
37
+ return json;
38
+ }
39
+ return {
40
+ ...(json['size'] === undefined
41
+ ? {}
42
+ : {
43
+ size: json['size'],
44
+ }),
45
+ ...(json['note'] === undefined
46
+ ? {}
47
+ : {
48
+ note: json['note'],
49
+ }),
50
+ };
51
+ },
52
+ };
53
+
54
+ public static UploadRequestContent = {
55
+ toJson: (model: UploadRequestContent): any => {
56
+ if (model === undefined || model === null) {
57
+ return model;
58
+ }
59
+ return {
60
+ ...(model.file === undefined
61
+ ? {}
62
+ : {
63
+ file: model.file,
64
+ }),
65
+ ...(model.note === undefined
66
+ ? {}
67
+ : {
68
+ note: model.note,
69
+ }),
70
+ ...(model.tags === undefined
71
+ ? {}
72
+ : {
73
+ tags: model.tags,
74
+ }),
75
+ };
76
+ },
77
+ fromJson: (json: any): UploadRequestContent => {
78
+ if (json === undefined || json === null) {
79
+ return json;
80
+ }
81
+ return {
82
+ file: json['file'],
83
+ ...(json['note'] === undefined
84
+ ? {}
85
+ : {
86
+ note: json['note'],
87
+ }),
88
+ ...(json['tags'] === undefined
89
+ ? {}
90
+ : {
91
+ tags: json['tags'],
92
+ }),
93
+ };
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Client configuration for TestApi
100
+ */
101
+ export interface TestApiConfig {
102
+ /**
103
+ * Base URL for the API
104
+ */
105
+ url: string;
106
+ /**
107
+ * Custom instance of fetch. By default the global 'fetch' is used.
108
+ * You can override this to add custom middleware for use cases such as adding authentication headers.
109
+ */
110
+ fetch?: typeof fetch;
111
+ /**
112
+ * Additional configuration
113
+ */
114
+ options?: {
115
+ /**
116
+ * By default, the client will add a Content-Type header, set to the media type defined for
117
+ * the request in the OpenAPI specification.
118
+ * Set this to false to omit this header.
119
+ */
120
+ omitContentTypeHeader?: boolean;
121
+ };
122
+ }
123
+
124
+ /**
125
+ * API Client for TestApi
126
+ */
127
+ export class TestApi {
128
+ private $config: TestApiConfig;
129
+
130
+ constructor(config: TestApiConfig) {
131
+ this.$config = config;
132
+
133
+ this.upload = this.upload.bind(this);
134
+ }
135
+
136
+ private $collectionDelimiters: { [format: string]: string } = {
137
+ csv: ',',
138
+ ssv: ' ',
139
+ pipes: '|',
140
+ };
141
+
142
+ private $deepObject = (key: string, value: any): string[] => {
143
+ if (value === undefined || value === null) {
144
+ return [];
145
+ }
146
+ if (typeof value !== 'object') {
147
+ return [\`\${key}=\${encodeURIComponent(String(value))}\`];
148
+ }
149
+ return Object.entries(value).flatMap(([prop, v]) =>
150
+ this.$deepObject(\`\${key}[\${encodeURIComponent(prop)}]\`, v),
151
+ );
152
+ };
153
+
154
+ private $url = (
155
+ path: string,
156
+ pathParameters: { [key: string]: any },
157
+ queryParameters: { [key: string]: any },
158
+ collectionFormats?: {
159
+ [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject';
160
+ },
161
+ ): string => {
162
+ const baseUrl = this.$config.url.endsWith('/')
163
+ ? this.$config.url.slice(0, -1)
164
+ : this.$config.url;
165
+ const pathWithParameters = Object.entries(pathParameters).reduce(
166
+ (withParams, [key, value]) =>
167
+ withParams.replace(\`{\${key}}\`, encodeURIComponent(\`\${value}\`)),
168
+ path,
169
+ );
170
+ const queryString = Object.entries(queryParameters)
171
+ .flatMap(([key, value]) => {
172
+ if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
173
+ return value.map(
174
+ (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
175
+ );
176
+ }
177
+ if (
178
+ collectionFormats?.[key] === 'deepObject' &&
179
+ value !== null &&
180
+ typeof value === 'object'
181
+ ) {
182
+ return this.$deepObject(encodeURIComponent(key), value);
183
+ }
184
+ const delimiter =
185
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
186
+ return [
187
+ \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}\`,
188
+ ];
189
+ })
190
+ .join('&');
191
+ return (
192
+ baseUrl + pathWithParameters + (queryString ? \`?\${queryString}\` : '')
193
+ );
194
+ };
195
+
196
+ private $headers = (
197
+ headerParameters: { [key: string]: any },
198
+ collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' },
199
+ ): [string, string][] => {
200
+ return Object.entries(headerParameters).flatMap(([key, value]) => {
201
+ if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
202
+ return value.map((v) => [key, String(v)]) as [string, string][];
203
+ }
204
+ const delimiter =
205
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
206
+ return [
207
+ [
208
+ key,
209
+ Array.isArray(value)
210
+ ? value.map(String).join(delimiter)
211
+ : String(value),
212
+ ],
213
+ ];
214
+ });
215
+ };
216
+
217
+ private $formData = (model: { [key: string]: any }): FormData => {
218
+ const formData = new FormData();
219
+ const append = (key: string, value: any): void => {
220
+ if (value === undefined || value === null) {
221
+ return;
222
+ }
223
+ if (value instanceof Blob || typeof value === 'string') {
224
+ formData.append(key, value);
225
+ } else if (Array.isArray(value)) {
226
+ value.forEach((v) => append(key, v));
227
+ } else if (typeof value === 'object') {
228
+ formData.append(key, JSON.stringify(value));
229
+ } else {
230
+ formData.append(key, String(value));
231
+ }
232
+ };
233
+ Object.entries(model).forEach(([key, value]) => append(key, value));
234
+ return formData;
235
+ };
236
+
237
+ private $fetch: typeof fetch = (...args) =>
238
+ (this.$config.fetch ?? fetch)(...args);
239
+
240
+ public async upload(input: UploadRequest): Promise<Upload200Response> {
241
+ const pathParameters: { [key: string]: any } = {};
242
+ const queryParameters: { [key: string]: any } = {};
243
+ const headerParameters: { [key: string]: any } = {};
244
+ const body = this.$formData($IO.UploadRequestContent.toJson(input));
245
+
246
+ const response = await this.$fetch(
247
+ this.$url('/upload', pathParameters, queryParameters),
248
+ {
249
+ headers: this.$headers(headerParameters),
250
+ method: 'POST',
251
+ body,
252
+ },
253
+ );
254
+
255
+ if (response.status === 200) {
256
+ return $IO.Upload200Response.fromJson(await response.json());
257
+ }
258
+ throw new Error(
259
+ \`Unknown response status \${response.status} returned by API\`,
260
+ );
261
+ }
262
+ }
263
+ "
264
+ `;
265
+
266
+ exports[`openApiTsClientGenerator - multipart/form-data > should match the generated client snapshot > types.gen.ts 1`] = `
267
+ "export type Upload200Response = {
268
+ size?: number;
269
+ note?: string;
270
+ };
271
+ export type UploadRequestContent = {
272
+ file: Blob;
273
+ note?: string;
274
+ tags?: Array<string>;
275
+ };
276
+
277
+ export type UploadRequest = UploadRequestContent;
278
+ export type UploadError = never;
279
+ "
280
+ `;
@@ -29,6 +29,13 @@ const composedMarshaller = (property, method) =>
29
29
  property.export === 'reference' && discriminatorBaseNames.has(property.type)
30
30
  ? `$IO.${property.typescriptType}.$${method}Base`
31
31
  : `$IO.${property.typescriptType}.${method}`;
32
+ // The single wire media type chosen for an operation's request body, if any.
33
+ const bodyMediaTypeOf = (op) => {
34
+ if (!op.parametersBody || !op.parametersBody.mediaTypes) return undefined;
35
+ const mediaTypes = Array.isArray(op.parametersBody.mediaTypes) ? op.parametersBody.mediaTypes : [op.parametersBody.mediaTypes];
36
+ return mediaTypes.find(mt => mt === 'application/json' || mt.endsWith('+json')) || mediaTypes[0];
37
+ };
38
+ const hasMultipartBody = allOperations.some(op => bodyMediaTypeOf(op) === 'multipart/form-data');
32
39
  _%>
33
40
  <%_ if ((models.length + allOperations.filter(p => p.parameters.length > 0).length) > 0) { _%>
34
41
  import type {
@@ -478,6 +485,28 @@ export class <%- className %> {
478
485
  });
479
486
  };
480
487
 
488
+ <%_ if (hasMultipartBody) { _%>
489
+ private $formData = (model: { [key: string]: any }): FormData => {
490
+ const formData = new FormData();
491
+ const append = (key: string, value: any): void => {
492
+ if (value === undefined || value === null) {
493
+ return;
494
+ }
495
+ if (value instanceof Blob || typeof value === 'string') {
496
+ formData.append(key, value);
497
+ } else if (Array.isArray(value)) {
498
+ value.forEach((v) => append(key, v));
499
+ } else if (typeof value === 'object') {
500
+ formData.append(key, JSON.stringify(value));
501
+ } else {
502
+ formData.append(key, String(value));
503
+ }
504
+ };
505
+ Object.entries(model).forEach(([key, value]) => append(key, value));
506
+ return formData;
507
+ };
508
+
509
+ <%_ } _%>
481
510
  private $fetch: typeof fetch = (...args) => (this.$config.fetch ?? fetch)(...args);
482
511
  <%_ allOperations.forEach((op) => { _%>
483
512
  <%_ const hasTag = op.tags && op.tags.length > 0; _%>
@@ -538,7 +567,13 @@ export class <%- className %> {
538
567
  } as const;
539
568
  <%_ } _%>
540
569
  <%_ if (op.parametersBody) { _%>
541
- <%_ if (op.parametersBody.isPrimitive && ['number', 'boolean', 'string'].includes(op.parametersBody.type) && !['array', 'dictionary'].includes(op.parametersBody.export) && !["date", "date-time"].includes(op.parametersBody.format)) { _%>
570
+ <%_ if (bodyMediaTypeOf(op) === 'multipart/form-data') { _%>
571
+ <%_ /* A multipart body is sent as FormData: Blob fields stream as file
572
+ parts, primitives serialise to strings, and fetch computes the
573
+ boundary itself (so the Content-Type header is intentionally unset
574
+ above). */ _%>
575
+ const body = <% if (!op.parametersBody.isRequired) { %>input === undefined ? undefined : <% } %>this.$formData(<%- op.explicitRequestBodyParameter ? `$IO.${op.operationIdPascalCase}RequestBodyParameters.toJson(input).${op.explicitRequestBodyParameter.prop}` : renderToJsonValue(op.parametersBody, 'input') %>);
576
+ <%_ } else if (op.parametersBody.isPrimitive && ['number', 'boolean', 'string'].includes(op.parametersBody.type) && !['array', 'dictionary'].includes(op.parametersBody.export) && !["date", "date-time"].includes(op.parametersBody.format)) { _%>
542
577
  const body = <% if (!op.parametersBody.isRequired) { %>input === undefined ? undefined : <% } %>String(input<%- op.explicitRequestBodyParameter ? `.${op.explicitRequestBodyParameter.typescriptName}` : '' %>);
543
578
  <%_ } else if (op.parametersBody.isPrimitive && ["date", "date-time"].includes(op.parametersBody.format)) { _%>
544
579
  const body = <%- renderToJsonDateValue('input', op.parametersBody.format) %>;
@@ -437,15 +437,22 @@ const hoistInlineObjectSubSchemas = (nameParts, schema, seenModelNameCounts)=>{
437
437
  }
438
438
  if ('requestBody' in operation) {
439
439
  const requestBody = resolveIfRef(spec, operation.requestBody);
440
- const jsonMediaType = preferredJsonMediaType(Object.keys(requestBody?.content ?? {}));
441
- const jsonRequestSchema = jsonMediaType ? requestBody.content[jsonMediaType].schema : undefined;
442
- if (jsonRequestSchema && !isRef(jsonRequestSchema) && ([
440
+ const contentMediaTypes = Object.keys(requestBody?.content ?? {});
441
+ // Hoist the JSON body, falling back to a form-data body (multipart or
442
+ // urlencoded) so an inline form object is fully typed and marshalled
443
+ // rather than left `unknown`.
444
+ const bodyMediaType = preferredJsonMediaType(contentMediaTypes) ?? contentMediaTypes.find((mt)=>[
445
+ 'multipart/form-data',
446
+ 'application/x-www-form-urlencoded'
447
+ ].includes(mt.split(';')[0]));
448
+ const bodyRequestSchema = bodyMediaType ? requestBody.content[bodyMediaType].schema : undefined;
449
+ if (bodyRequestSchema && !isRef(bodyRequestSchema) && ([
443
450
  'object',
444
451
  'array'
445
- ].includes(jsonRequestSchema.type) || isCompositeSchema(jsonRequestSchema) || jsonRequestSchema?.type === 'string' && jsonRequestSchema.enum)) {
452
+ ].includes(bodyRequestSchema.type) || isCompositeSchema(bodyRequestSchema) || bodyRequestSchema?.type === 'string' && bodyRequestSchema.enum)) {
446
453
  const schemaName = `${upperFirst(deduplicatedOpId)}RequestContent`;
447
- spec.components.schemas[schemaName] = jsonRequestSchema;
448
- requestBody.content[jsonMediaType].schema = {
454
+ spec.components.schemas[schemaName] = bodyRequestSchema;
455
+ requestBody.content[bodyMediaType].schema = {
449
456
  $ref: `#/components/schemas/${schemaName}`
450
457
  };
451
458
  }