@stackwright-pro/openapi 0.3.0-alpha.6 → 0.3.0-alpha.7

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/dist/index.mjs CHANGED
@@ -148,8 +148,14 @@ var SchemaResolver = class {
148
148
  const response = responses[responseCode];
149
149
  if (!response) return void 0;
150
150
  for (const contentType of SUPPORTED_CONTENT_TYPES) {
151
- const content = response.content?.[contentType];
152
- if (!content?.schema) continue;
151
+ const responseContent = response.content;
152
+ if (!responseContent) continue;
153
+ const exactContent = responseContent[contentType];
154
+ const normalizedEntry = exactContent ? void 0 : Object.entries(responseContent).find(
155
+ ([k]) => k === contentType || k.startsWith(`${contentType};`)
156
+ );
157
+ const content = exactContent ?? normalizedEntry?.[1];
158
+ if (!content || !content.schema) continue;
153
159
  const schema = content.schema;
154
160
  if ("$ref" in schema) {
155
161
  console.warn(
@@ -279,7 +285,7 @@ ${body}`;
279
285
  const isNullable = "nullable" in schema && schema.nullable === true;
280
286
  let baseSchema = this.schemaTypeToZod(schema);
281
287
  if (schema.description) {
282
- const escapedDesc = schema.description.replace(/'/g, "\\'");
288
+ const escapedDesc = schema.description.replace(/'/g, "\\'").replace(/\r?\n/g, " ");
283
289
  baseSchema += `.describe('${escapedDesc}')`;
284
290
  }
285
291
  if (isNullable) {
@@ -417,7 +423,7 @@ ${body}`;
417
423
  if (!schema.properties || Object.keys(schema.properties).length === 0) {
418
424
  if (schema.additionalProperties) {
419
425
  const valueSchema = typeof schema.additionalProperties === "object" ? this.schemaToZod(schema.additionalProperties) : "z.unknown()";
420
- return `z.record(${valueSchema})`;
426
+ return `z.record(z.string(), ${valueSchema})`;
421
427
  }
422
428
  return "z.object({})";
423
429
  }
@@ -614,10 +620,10 @@ var CollectionProviderGenerator = class {
614
620
  const authHeader = this.generateAuthHeader(auth);
615
621
  const arraySchemaName = `${schemaName.replace(/Schema$/, "")}ArraySchema`;
616
622
  const validationSchema = unknownFallback ? "z.unknown()" : isArray ? arraySchemaName : schemaName;
617
- const imports = bare ? "" : unknownFallback ? `import type { CollectionProvider, CollectionItem } from '@stackwright/collections';
623
+ const imports = bare ? "" : unknownFallback ? `import type { CollectionProvider, CollectionEntry, CollectionListOptions, CollectionListResult } from '@stackwright/collections';
618
624
  import { z } from 'zod';
619
625
 
620
- ` : `import type { CollectionProvider, CollectionItem } from '@stackwright/collections';
626
+ ` : `import type { CollectionProvider, CollectionEntry, CollectionListOptions, CollectionListResult } from '@stackwright/collections';
621
627
  import { ${isArray ? arraySchemaName : schemaName} } from './schemas';
622
628
 
623
629
  `;
@@ -636,29 +642,20 @@ export class ${providerName} implements CollectionProvider {
636
642
  }
637
643
 
638
644
  /**
639
- * Get collection name
645
+ * List available collection names.
640
646
  */
641
- getName(): string {
642
- return '${collectionName}';
647
+ async collections(): Promise<string[]> {
648
+ return ['${collectionName}'];
643
649
  }
644
650
 
645
651
  /**
646
652
  * List all items in the collection
647
653
  */
648
- async list(options?: {
649
- limit?: number;
650
- offset?: number;
651
- filters?: Record<string, unknown>;
652
- }): Promise<CollectionItem[]> {
654
+ async list(_collection: string, opts?: CollectionListOptions): Promise<CollectionListResult> {
653
655
  const url = new URL(\`\${this.baseUrl}${endpoint}\`);
654
656
 
655
- // Add pagination/filter params if supported
656
- if (options?.limit) {
657
- url.searchParams.set('limit', String(options.limit));
658
- }
659
- if (options?.offset) {
660
- url.searchParams.set('offset', String(options.offset));
661
- }
657
+ if (opts?.limit) url.searchParams.set('limit', String(opts.limit));
658
+ if (opts?.offset) url.searchParams.set('offset', String(opts.offset));
662
659
 
663
660
  const response = await fetch(url.toString(), {
664
661
  method: '${method.toUpperCase()}',
@@ -674,29 +671,24 @@ export class ${providerName} implements CollectionProvider {
674
671
  // Validate response with Zod schema
675
672
  const validated = ${validationSchema}.parse(data);
676
673
 
677
- // Convert to CollectionItem format
678
- return ${isArray ? "validated" : "[validated]"}.map((item: any) => this.toCollectionItem(item));
674
+ const entries = ${isArray ? "validated" : "[validated]"}.map((item: any) => this.toCollectionEntry(item));
675
+ return { entries, total: entries.length };
679
676
  }
680
677
 
681
678
  /**
682
679
  * Get a single item by slug
683
680
  */
684
- async get(slug: string): Promise<CollectionItem | null> {
681
+ async get(_collection: string, slug: string): Promise<CollectionEntry | null> {
685
682
  ${this.generateGetMethod(endpoint, slugField, isArray, collectionName, schemaName, unknownFallback)}
686
683
  }
687
684
 
688
685
  /**
689
- * Convert API response to CollectionItem
686
+ * Convert API response to CollectionEntry
690
687
  */
691
- private toCollectionItem(item: any): CollectionItem {
688
+ private toCollectionEntry(item: any): CollectionEntry {
692
689
  return {
693
690
  slug: String(item.${slugField}),
694
- title: item.${this.guessTitle(slugField)},
695
- data: item,
696
- metadata: {
697
- source: '${collectionName}',
698
- _raw: item,
699
- },
691
+ ...item,
700
692
  };
701
693
  }
702
694
 
@@ -739,10 +731,10 @@ export class ${providerName} implements CollectionProvider {
739
731
  const data = await response.json();
740
732
  const validated = ${validationExpr}.parse(data);
741
733
 
742
- return this.toCollectionItem(validated);`;
734
+ return this.toCollectionEntry(validated);`;
743
735
  } else {
744
- return `const items = await this.list();
745
- return items.find((item) => item.slug === slug) || null;`;
736
+ return `const result = await this.list(_collection);
737
+ return result.entries.find((item) => item.slug === slug) ?? null;`;
746
738
  }
747
739
  }
748
740
  /**
@@ -990,13 +982,25 @@ import type { CollectionActionRegistry } from '@stackwright-pro/workflow';
990
982
  export const actions: CollectionActionRegistry = {};
991
983
  `;
992
984
  }
993
- /** Derives the client method name from endpoint + method (best-effort) */
985
+ /** Derives the client method name from endpoint + method.
986
+ * Prefers operationId (matching ClientGenerator.getMethodName()), falls back
987
+ * to path+method derivation for specs that omit operationIds. */
994
988
  endpointToClientMethod(endpoint, method) {
989
+ const pathItem = this.document.paths?.[endpoint];
990
+ const operation = pathItem?.[method.toLowerCase()];
991
+ if (operation?.operationId) {
992
+ return this.camelCase(operation.operationId);
993
+ }
995
994
  const parts = endpoint.split("/").filter(Boolean).filter((p) => !p.startsWith("{")).map((p) => p.replace(/-/g, "_"));
996
995
  const prefix = method.toLowerCase();
997
996
  const suffix = parts.map((p) => this.capitalize(p)).join("");
998
997
  return `${prefix}${suffix}`;
999
998
  }
999
+ /** Convert any string to camelCase — handles operationId values like
1000
+ * createOrder, list_items, get-by-id, etc. */
1001
+ camelCase(str) {
1002
+ return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()).replace(/^[A-Z]/, (chr) => chr.toLowerCase());
1003
+ }
1000
1004
  toCamelCase(str) {
1001
1005
  const parts = str.split(/[-_]/);
1002
1006
  return parts.map((p, i) => i === 0 ? p.toLowerCase() : this.capitalize(p)).join("");
@@ -1017,11 +1021,13 @@ var ClientGenerator = class {
1017
1021
  }
1018
1022
  this.requiredSchemas = /* @__PURE__ */ new Set();
1019
1023
  this.generatedRequestSchemas = /* @__PURE__ */ new Set();
1024
+ this.generatedRequestTypes = /* @__PURE__ */ new Set();
1020
1025
  }
1021
1026
  resolver;
1022
1027
  schemaMapping;
1023
1028
  requiredSchemas;
1024
1029
  generatedRequestSchemas;
1030
+ generatedRequestTypes;
1025
1031
  /**
1026
1032
  * Generate typed API client code from OpenAPI document
1027
1033
  */
@@ -1040,12 +1046,11 @@ var ClientGenerator = class {
1040
1046
  }
1041
1047
  this.requiredSchemas.clear();
1042
1048
  this.generatedRequestSchemas.clear();
1049
+ this.generatedRequestTypes.clear();
1043
1050
  let code = this.generateImports(!!schemaMapping, validateResponses);
1044
1051
  code += "\n";
1045
- if (schemaMapping) {
1046
- code += this.generateRequestSchemas(endpoints);
1047
- code += "\n";
1048
- }
1052
+ code += this.generateRequestSchemas(endpoints);
1053
+ code += "\n";
1049
1054
  code += this.generateTypes(endpoints, schemaMapping);
1050
1055
  code += "\n";
1051
1056
  code += this.generateErrorClasses();
@@ -1076,9 +1081,9 @@ var ClientGenerator = class {
1076
1081
  */
1077
1082
 
1078
1083
  `;
1079
- if (useSchemas && validateResponses) {
1080
- code += `import { z } from 'zod';
1084
+ code += `import { z } from 'zod';
1081
1085
  `;
1086
+ if (useSchemas && validateResponses) {
1082
1087
  code += `import * as schemas from './schemas';
1083
1088
  `;
1084
1089
  }
@@ -1111,6 +1116,9 @@ var ClientGenerator = class {
1111
1116
  const schemaName = `${typeName}RequestSchema`;
1112
1117
  const schemaCode = this.generateRequestSchemaForEndpoint(endpoint);
1113
1118
  if (schemaCode) {
1119
+ if (this.generatedRequestSchemas.has(schemaName)) {
1120
+ continue;
1121
+ }
1114
1122
  code += `export const ${schemaName} = ${schemaCode};
1115
1123
 
1116
1124
  `;
@@ -1162,7 +1170,8 @@ ${parts.join(",\n")}
1162
1170
  for (const param of params) {
1163
1171
  const zodSchema = this.parameterSchemaToZod(param);
1164
1172
  const desc = param.description ? `.describe('${this.escapeString(param.description)}')` : "";
1165
- paramSchemas.push(` ${param.name}: ${zodSchema}${desc}`);
1173
+ const safeParamKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `'${param.name}'`;
1174
+ paramSchemas.push(` ${safeParamKey}: ${zodSchema}${desc}`);
1166
1175
  }
1167
1176
  return `z.object({
1168
1177
  ${paramSchemas.join(",\n")}
@@ -1181,7 +1190,8 @@ ${paramSchemas.join(",\n")}
1181
1190
  if (!param.required) {
1182
1191
  zodSchema += ".optional()";
1183
1192
  }
1184
- paramSchemas.push(` ${param.name}: ${zodSchema}`);
1193
+ const safeParamKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `'${param.name}'`;
1194
+ paramSchemas.push(` ${safeParamKey}: ${zodSchema}`);
1185
1195
  }
1186
1196
  return `z.object({
1187
1197
  ${paramSchemas.join(",\n")}
@@ -1254,7 +1264,7 @@ ${paramSchemas.join(",\n")}
1254
1264
  }
1255
1265
  if (schema.type === "object") {
1256
1266
  if (!schema.properties) {
1257
- return "z.record(z.unknown())";
1267
+ return "z.record(z.string(), z.unknown())";
1258
1268
  }
1259
1269
  const props = [];
1260
1270
  const required = schema.required || [];
@@ -1264,7 +1274,8 @@ ${paramSchemas.join(",\n")}
1264
1274
  if (!isRequired) {
1265
1275
  propZod += ".optional()";
1266
1276
  }
1267
- props.push(`${key}: ${propZod}`);
1277
+ const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
1278
+ props.push(`${safeKey}: ${propZod}`);
1268
1279
  }
1269
1280
  return `z.object({ ${props.join(", ")} })`;
1270
1281
  }
@@ -1422,13 +1433,24 @@ ${paramSchemas.join(",\n")}
1422
1433
  const operationId = endpoint.operationId || this.getMethodName(endpoint);
1423
1434
  const mapping = schemaMapping[operationId];
1424
1435
  if (!mapping) {
1436
+ const typeName2 = this.getOperationTypeName(endpoint);
1437
+ const requestSchemaName2 = `${typeName2}RequestSchema`;
1438
+ if (this.generatedRequestSchemas.has(requestSchemaName2) && !this.generatedRequestTypes.has(requestSchemaName2)) {
1439
+ code += `export type ${typeName2}Request = z.infer<typeof ${requestSchemaName2}>;
1440
+ `;
1441
+ this.generatedRequestTypes.add(requestSchemaName2);
1442
+ }
1443
+ code += `export type ${typeName2}Response = unknown;
1444
+
1445
+ `;
1425
1446
  continue;
1426
1447
  }
1427
1448
  const typeName = this.getOperationTypeName(endpoint);
1428
1449
  const requestSchemaName = mapping.requestSchema || typeName + "RequestSchema";
1429
- if (this.generatedRequestSchemas.has(requestSchemaName)) {
1450
+ if (this.generatedRequestSchemas.has(requestSchemaName) && !this.generatedRequestTypes.has(requestSchemaName)) {
1430
1451
  this.addRequiredSchema(requestSchemaName);
1431
1452
  code += "export type " + typeName + "Request = z.infer<typeof " + requestSchemaName + ">;\n";
1453
+ this.generatedRequestTypes.add(requestSchemaName);
1432
1454
  }
1433
1455
  this.addRequiredSchema(mapping.responseSchema);
1434
1456
  code += `export type ${typeName}Response = z.infer<typeof schemas.${mapping.responseSchema}>;
@@ -1473,8 +1495,9 @@ ${paramSchemas.join(",\n")}
1473
1495
  const type = this.getTypeFromSchemaLegacy(param.schema);
1474
1496
  const desc = param.description ? ` /** ${param.description} */
1475
1497
  ` : "";
1498
+ const safeParamKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `'${param.name}'`;
1476
1499
  code += desc;
1477
- code += ` ${param.name}${required}: ${type};
1500
+ code += ` ${safeParamKey}${required}: ${type};
1478
1501
  `;
1479
1502
  }
1480
1503
  code += " };\n";
@@ -1487,8 +1510,9 @@ ${paramSchemas.join(",\n")}
1487
1510
  const type = this.getTypeFromSchemaLegacy(param.schema);
1488
1511
  const desc = param.description ? ` /** ${param.description} */
1489
1512
  ` : "";
1513
+ const safeParamKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `'${param.name}'`;
1490
1514
  code += desc;
1491
- code += ` ${param.name}${required}: ${type};
1515
+ code += ` ${safeParamKey}${required}: ${type};
1492
1516
  `;
1493
1517
  }
1494
1518
  code += " };\n";
@@ -1501,8 +1525,9 @@ ${paramSchemas.join(",\n")}
1501
1525
  const type = this.getTypeFromSchemaLegacy(param.schema);
1502
1526
  const desc = param.description ? ` /** ${param.description} */
1503
1527
  ` : "";
1528
+ const safeParamKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `'${param.name}'`;
1504
1529
  code += desc;
1505
- code += ` ${param.name}${required}: ${type};
1530
+ code += ` ${safeParamKey}${required}: ${type};
1506
1531
  `;
1507
1532
  }
1508
1533
  code += " };\n";
@@ -1849,9 +1874,10 @@ ${paramSchemas.join(",\n")}
1849
1874
  code += ` if (request.query) {
1850
1875
  `;
1851
1876
  for (const param of queryParams) {
1852
- code += ` if (request.query.${param.name} != null) {
1877
+ const safeAccess = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? `.${param.name}` : `['${param.name}']`;
1878
+ code += ` if (request.query${safeAccess} != null) {
1853
1879
  `;
1854
- code += ` searchParams.append('${param.name}', String(request.query.${param.name}));
1880
+ code += ` searchParams.append('${param.name}', String(request.query${safeAccess}));
1855
1881
  `;
1856
1882
  code += ` }
1857
1883
  `;
@@ -1875,7 +1901,7 @@ ${paramSchemas.join(",\n")}
1875
1901
  code += ` method: '${method.toUpperCase()}',
1876
1902
  `;
1877
1903
  if (hasBody) {
1878
- code += ` body: request.body ? JSON.stringify(request.body) : null,
1904
+ code += ` body: request.body ? JSON.stringify(request.body) : undefined,
1879
1905
  `;
1880
1906
  }
1881
1907
  if (headerParams.length > 0) {
@@ -1992,9 +2018,10 @@ ${paramSchemas.join(",\n")}
1992
2018
  code += ` if (request.query) {
1993
2019
  `;
1994
2020
  for (const param of queryParams) {
1995
- code += ` if (request.query.${param.name} != null) {
2021
+ const safeAccess = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? `.${param.name}` : `['${param.name}']`;
2022
+ code += ` if (request.query${safeAccess} != null) {
1996
2023
  `;
1997
- code += ` searchParams.append('${param.name}', String(request.query.${param.name}));
2024
+ code += ` searchParams.append('${param.name}', String(request.query${safeAccess}));
1998
2025
  `;
1999
2026
  code += ` }
2000
2027
  `;
@@ -2016,7 +2043,7 @@ ${paramSchemas.join(",\n")}
2016
2043
  code += ` method: '${method.toUpperCase()}',
2017
2044
  `;
2018
2045
  if (hasBody) {
2019
- code += ` body: request.body ? JSON.stringify(request.body) : null,
2046
+ code += ` body: request.body ? JSON.stringify(request.body) : undefined,
2020
2047
  `;
2021
2048
  }
2022
2049
  const headerParams = params.filter((p) => p.in === "header");
@@ -2182,7 +2209,7 @@ ${paramSchemas.join(",\n")}
2182
2209
  if (endpoint.operationId) {
2183
2210
  return this.camelCase(endpoint.operationId);
2184
2211
  }
2185
- const pathParts = endpoint.path.split("/").filter((p) => p && !p.startsWith("{")).join("-");
2212
+ const pathParts = endpoint.path.split("/").filter((p) => p).map((p) => p.startsWith("{") && p.endsWith("}") ? p.slice(1, -1) : p).join("-");
2186
2213
  return this.camelCase(`${endpoint.method}-${pathParts}`);
2187
2214
  }
2188
2215
  /**
@@ -2988,6 +3015,7 @@ import { z } from 'zod';
2988
3015
  if (!operation) continue;
2989
3016
  const opId = operation.operationId || this.generateOperationId(pathStr, method);
2990
3017
  const responseSchemaName = `${this.getOperationTypeName(opId)}ResponseSchema`;
3018
+ if (!generatedSchemas.has(responseSchemaName)) continue;
2991
3019
  schemaMapping[opId] = {
2992
3020
  requestSchema: null,
2993
3021
  // ClientGenerator handles request schema generation + type inference
@@ -3012,7 +3040,13 @@ import type * as schemas from './schemas';
3012
3040
 
3013
3041
  `;
3014
3042
  if (collections) {
3043
+ const resolver = new SchemaResolver(document);
3015
3044
  for (const collection of collections) {
3045
+ const schema = resolver.getResponseSchema(
3046
+ collection.endpoint,
3047
+ collection.method?.toLowerCase() ?? "get"
3048
+ );
3049
+ if (!schema) continue;
3016
3050
  const collectionName = this.sanitizeName(collection.endpoint);
3017
3051
  const typeName = this.capitalize(collectionName);
3018
3052
  const schemaName = `${typeName}Schema`;
@@ -3058,13 +3092,20 @@ import type * as schemas from './schemas';
3058
3092
  collection.endpoint,
3059
3093
  collection.method?.toLowerCase() ?? "get"
3060
3094
  );
3061
- const isArray = schema?.type === "array";
3062
- if (isArray) {
3063
- schemaImports.add(`${this.capitalize(collectionName)}ArraySchema`);
3064
- } else {
3065
- schemaImports.add(`${this.capitalize(collectionName)}Schema`);
3095
+ if (schema) {
3096
+ const isArray = schema.type === "array";
3097
+ if (isArray) {
3098
+ schemaImports.add(`${this.capitalize(collectionName)}ArraySchema`);
3099
+ } else {
3100
+ schemaImports.add(`${this.capitalize(collectionName)}Schema`);
3101
+ }
3066
3102
  }
3067
3103
  }
3104
+ const needsZodImport = classBlocks.some((block) => block.includes("z.unknown()"));
3105
+ const zodImportLine = needsZodImport ? `import { z } from 'zod';
3106
+ ` : "";
3107
+ const schemaImportLine = schemaImports.size > 0 ? `import { ${Array.from(schemaImports).join(", ")} } from './schemas';
3108
+ ` : "";
3068
3109
  let providerCode = `/**
3069
3110
  * Generated CollectionProvider from OpenAPI spec
3070
3111
  * Integration: ${integrationName}
@@ -3073,9 +3114,8 @@ import type * as schemas from './schemas';
3073
3114
  * Regenerate by running: pnpm prebuild
3074
3115
  */
3075
3116
 
3076
- import type { CollectionProvider, CollectionItem } from '@stackwright/collections';
3077
- import { ${Array.from(schemaImports).join(", ")} } from './schemas';
3078
-
3117
+ import type { CollectionProvider, CollectionEntry, CollectionListOptions, CollectionListResult } from '@stackwright/collections';
3118
+ ${zodImportLine}${schemaImportLine}
3079
3119
  `;
3080
3120
  providerCode += classBlocks.join("\n");
3081
3121
  fs2.writeFileSync(path2.join(outputDir, "provider.ts"), providerCode);