@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 +2 -2
- package/src/api-catalog.ts +4 -2
- package/src/api-file-fields.ts +20 -4
- package/src/index.ts +1 -0
- package/src/openapi-import.ts +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sungen/driver-api",
|
|
3
|
-
"version": "3.2.
|
|
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.
|
|
16
|
+
"@sun-asterisk/sungen": "3.2.8",
|
|
17
17
|
"commander": "^14.0.2",
|
|
18
18
|
"yaml": "^2.8.2"
|
|
19
19
|
},
|
package/src/api-catalog.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
}
|
package/src/api-file-fields.ts
CHANGED
|
@@ -31,12 +31,28 @@ export function parseFileSpec(v: any): FileFieldSpec | undefined {
|
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
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 =
|
|
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)}` : '';
|
package/src/openapi-import.ts
CHANGED
|
@@ -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]))
|
|
143
|
-
|
|
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 } : {}),
|