@sungen/driver-api 3.2.7 → 3.2.8

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": "@sungen/driver-api",
3
- "version": "3.2.7",
3
+ "version": "3.2.8",
4
4
  "description": "Sungen API capability — the @api annotation, API catalog, OpenAPI import + `sungen api import`, and the specs/api.ts runtime template. Plugs into @sun-asterisk/sungen via the capability SPI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "author": "eqe team (engineer & quality) — Sun Asterisk",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@sun-asterisk/sungen": "3.2.7",
16
+ "@sun-asterisk/sungen": "3.2.8",
17
17
  "commander": "^14.0.2",
18
18
  "yaml": "^2.8.2"
19
19
  },
@@ -33,7 +33,7 @@ export interface ApiEntry {
33
33
  path: string;
34
34
  body?: unknown;
35
35
  encoding?: 'json' | 'form' | 'multipart'; // request-body wire format (default json); form = application/x-www-form-urlencoded
36
- files?: Record<string, FileFieldSpec>; // multipart/form-data file fields (file as a form part) presence forces multipart
36
+ files?: Record<string, FileFieldSpec | FileFieldSpec[]>; // multipart/form-data file fields array = multiple files under one field name; presence forces multipart
37
37
  bodyFile?: FileFieldSpec; // raw-binary body: the request body IS the file's bytes (Content-Type = mimeType, default application/octet-stream)
38
38
  headers?: Record<string, string>; // per-endpoint header templates; `:param` tokens bind at runtime (flow auth, e.g. authorization: "Bearer :token")
39
39
  params: string[];
@@ -170,7 +170,9 @@ export function validateApiEntry(entry: ApiEntry, datasourceNames: string[] | nu
170
170
  for (const r of refs) if (!declared.has(r)) errs.push(`api "${entry.name}": uses :${r} but it is not in params:.`);
171
171
  for (const d of declared) if (!refs.has(d)) errs.push(`api "${entry.name}": param "${d}" is declared but never used in path/body.`);
172
172
  if (entry.files) {
173
- for (const [field, spec] of Object.entries(entry.files)) if (!spec.path) errs.push(`api "${entry.name}": file field "${field}" has no path.`);
173
+ for (const [field, raw] of Object.entries(entry.files)) {
174
+ for (const spec of Array.isArray(raw) ? raw : [raw]) if (!spec.path) errs.push(`api "${entry.name}": file field "${field}" has no path.`);
175
+ }
174
176
  // files are always sent as multipart; an explicit json/form encoding contradicts that.
175
177
  if (entry.encoding && entry.encoding !== 'multipart') errs.push(`api "${entry.name}": has files: but encoding: ${entry.encoding} — a file upload must be multipart (omit encoding).`);
176
178
  }
@@ -31,12 +31,28 @@ export function parseFileSpec(v: any): FileFieldSpec | undefined {
31
31
  return undefined;
32
32
  }
33
33
 
34
- /** Normalize the `files:` block — each value via parseFileSpec. Undefined when no valid fields. */
35
- export function parseFiles(raw: any): Record<string, FileFieldSpec> | undefined {
34
+ /**
35
+ * Normalize one field's value — a single spec, OR an array of specs to send MULTIPLE files under the
36
+ * SAME field name (e.g. `gift_image: [":a", ":b"]`). A single file stays a single spec (object), so
37
+ * existing single-file catalogs are byte-unchanged. An array with no valid element → undefined.
38
+ */
39
+ export function parseFilesValue(v: any): FileFieldSpec | FileFieldSpec[] | undefined {
40
+ if (Array.isArray(v)) {
41
+ if (!v.length) return undefined;
42
+ // Preserve a malformed element as an empty-path spec instead of silently dropping it: a typo'd
43
+ // entry (e.g. missing `path`) must surface as a validation error ("file field has no path"), not
44
+ // quietly shrink the upload to fewer files than the author declared.
45
+ return v.map((el) => parseFileSpec(el) ?? { path: '' });
46
+ }
47
+ return parseFileSpec(v);
48
+ }
49
+
50
+ /** Normalize the `files:` block — each value via parseFilesValue. Undefined when no valid fields. */
51
+ export function parseFiles(raw: any): Record<string, FileFieldSpec | FileFieldSpec[]> | undefined {
36
52
  if (!raw || typeof raw !== 'object') return undefined;
37
- const out: Record<string, FileFieldSpec> = {};
53
+ const out: Record<string, FileFieldSpec | FileFieldSpec[]> = {};
38
54
  for (const [field, v] of Object.entries(raw as Record<string, any>)) {
39
- const spec = parseFileSpec(v);
55
+ const spec = parseFilesValue(v);
40
56
  if (spec) out[field] = spec;
41
57
  }
42
58
  return Object.keys(out).length ? out : undefined;
package/src/index.ts CHANGED
@@ -94,6 +94,7 @@ function apiPreconditionCodegen(input: { tags: string[]; screenName: string; cwd
94
94
  const enc = entry.encoding ? `, encoding: ${JSON.stringify(entry.encoding)}` : '';
95
95
  // multipart file fields; the runtime reads each fixture and builds a Playwright file payload. Its
96
96
  // `:param` paths bind at runtime from the same params. Presence forces multipart in the runtime.
97
+ // A field's value may be an array → multiple files under one field name (JSON.stringify carries it).
97
98
  const files = entry.files && Object.keys(entry.files).length ? `, files: ${JSON.stringify(entry.files)}` : '';
98
99
  // raw-binary body upload: the request body IS the fixture's bytes (e.g. application/octet-stream).
99
100
  const bodyFile = entry.bodyFile ? `, bodyFile: ${JSON.stringify(entry.bodyFile)}` : '';
@@ -18,7 +18,7 @@ export interface ImportedApiEntry {
18
18
  path: string;
19
19
  params?: string[];
20
20
  body?: Record<string, string>;
21
- files?: Record<string, { path: string; mimeType?: string; filename?: string; name?: string }>; // multipart/form-data binary props → file form fields
21
+ files?: Record<string, { path: string; mimeType?: string; filename?: string; name?: string } | { path: string; mimeType?: string; filename?: string; name?: string }[]>; // multipart/form-data binary props → file form fields (array = multiple files under one field)
22
22
  bodyFile?: { path: string; mimeType?: string; filename?: string; name?: string }; // raw-binary request body (application/octet-stream) → the body IS the file
23
23
  encoding?: 'form' | 'multipart'; // set when the requestBody is form/multipart (json omitted = default)
24
24
  headers?: Record<string, string>; // per-endpoint header templates (inferred auth, e.g. authorization: "Bearer :token")
@@ -111,7 +111,7 @@ function resolveBodySchema(schema: any, components: Record<string, any>, seen: S
111
111
  * and any `files` (multipart binary props), best-effort from the requestBody content-type (#345).
112
112
  * JSON is the default (encoding omitted). Also returns the picked request schema (for field metadata),
113
113
  * resolving a `$ref`/`allOf` body against the doc's component schemas. */
114
- function bodyTemplate(op: any, components: Record<string, any> = {}): { body?: Record<string, string>; files?: Record<string, { path: string }>; bodyFile?: { path: string; mimeType?: string }; props: string[]; encoding?: 'form' | 'multipart'; schema?: any } {
114
+ function bodyTemplate(op: any, components: Record<string, any> = {}): { body?: Record<string, string>; files?: Record<string, { path: string } | { path: string }[]>; bodyFile?: { path: string; mimeType?: string }; props: string[]; encoding?: 'form' | 'multipart'; schema?: any } {
115
115
  const content = op?.requestBody?.content ?? {};
116
116
  // A raw binary body → the request body IS the file's bytes, NOT a multipart part (multipart would
117
117
  // be rejected 415). This is `application/octet-stream` (Petstore `uploadImage`) OR a concrete media
@@ -135,12 +135,16 @@ function bodyTemplate(op: any, components: Record<string, any> = {}): { body?: R
135
135
  const properties = schema && typeof schema === 'object' && schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
136
136
  const propNames = Object.keys(properties);
137
137
  if (!propNames.length) return { props: [], encoding: pick.encoding, schema };
138
- // multipart: split binary (file) props into `files`, the rest stay text `body`.
138
+ // multipart: split binary (file) props into `files`, the rest stay text `body`. An array-of-binary
139
+ // prop (`type: array, items.format: binary`) → an array file value, signalling "multiple files under
140
+ // one field": the importer seeds one templated entry; the author adds more `:param` paths as needed.
139
141
  const body: Record<string, string> = {};
140
- const files: Record<string, { path: string }> = {};
142
+ const files: Record<string, { path: string } | { path: string }[]> = {};
141
143
  for (const p of propNames) {
142
- if (pick.encoding === 'multipart' && isBinaryProp(properties[p])) files[p] = { path: `:${p}` };
143
- else body[p] = `:${p}`;
144
+ if (pick.encoding === 'multipart' && isBinaryProp(properties[p])) {
145
+ const prop = properties[p];
146
+ files[p] = prop?.type === 'array' ? [{ path: `:${p}` }] : { path: `:${p}` };
147
+ } else body[p] = `:${p}`;
144
148
  }
145
149
  return {
146
150
  ...(Object.keys(body).length ? { body } : {}),