zenko 0.1.0 → 0.1.1

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/cli.mjs CHANGED
@@ -214,11 +214,13 @@ function generate(spec, options = {}) {
214
214
  const output = [];
215
215
  const generatedTypes = /* @__PURE__ */ new Set();
216
216
  const { strictDates = false, strictNumeric = false } = options;
217
+ const typesConfig = normalizeTypesConfig(options.types);
217
218
  const schemaOptions = {
218
219
  strictDates,
219
220
  strictNumeric
220
221
  };
221
222
  output.push('import { z } from "zod";');
223
+ appendHelperTypesImport(output, typesConfig);
222
224
  output.push("");
223
225
  if (spec.components?.schemas) {
224
226
  output.push("// Generated Zod Schemas");
@@ -238,16 +240,79 @@ function generate(spec, options = {}) {
238
240
  output.push("// Path Functions");
239
241
  output.push("export const paths = {");
240
242
  for (const op of operations) {
241
- if (op.pathParams.length === 0) {
243
+ const pathParamNames = op.pathParams.map((p) => p.name);
244
+ const hasPathParams = pathParamNames.length > 0;
245
+ const hasQueryParams = op.queryParams.length > 0;
246
+ if (!hasPathParams && !hasQueryParams) {
242
247
  output.push(` ${op.operationId}: () => "${op.path}",`);
243
- } else {
244
- const paramNames = op.pathParams.map((p) => p.name).join(", ");
245
- const paramTypes = op.pathParams.map((p) => `${p.name}: string`).join(", ");
246
- const pathWithParams = op.path.replace(/{([^}]+)}/g, "${$1}");
248
+ continue;
249
+ }
250
+ const allParamNames = [
251
+ ...pathParamNames,
252
+ ...op.queryParams.map((p) => p.name)
253
+ ];
254
+ const signaturePieces = [];
255
+ for (const param of op.pathParams) {
256
+ signaturePieces.push(`${param.name}: string`);
257
+ }
258
+ for (const param of op.queryParams) {
259
+ signaturePieces.push(
260
+ `${param.name}${param.required ? "" : "?"}: ${mapQueryType(param)}`
261
+ );
262
+ }
263
+ const signatureParams = signaturePieces.join(", ");
264
+ const needsDefaultObject = !hasPathParams && hasQueryParams && op.queryParams.every((param) => !param.required);
265
+ const signatureArgs = allParamNames.length ? `{ ${allParamNames.join(", ")} }` : "{}";
266
+ const signature = `${signatureArgs}: { ${signatureParams} }${needsDefaultObject ? " = {}" : ""}`;
267
+ const pathWithParams = op.path.replace(/{([^}]+)}/g, "${$1}");
268
+ if (!hasQueryParams) {
247
269
  output.push(
248
- ` ${op.operationId}: ({ ${paramNames} }: { ${paramTypes} }) => \`${pathWithParams}\`,`
270
+ ` ${op.operationId}: (${signature}) => \`${pathWithParams}\`,`
249
271
  );
272
+ continue;
273
+ }
274
+ output.push(` ${op.operationId}: (${signature}) => {`);
275
+ output.push(" const params = new URLSearchParams()");
276
+ for (const param of op.queryParams) {
277
+ const propertyKey = formatPropertyName(param.name);
278
+ const accessor = isValidJSIdentifier(param.name) ? param.name : propertyKey;
279
+ const schema = param.schema ?? {};
280
+ if (schema?.type === "array") {
281
+ const itemValueExpression = convertQueryParamValue(
282
+ schema.items ?? {},
283
+ "value"
284
+ );
285
+ if (param.required) {
286
+ output.push(` for (const value of ${accessor}) {`);
287
+ output.push(
288
+ ` params.append("${param.name}", ${itemValueExpression})`
289
+ );
290
+ output.push(" }");
291
+ } else {
292
+ output.push(` if (${accessor} !== undefined) {`);
293
+ output.push(` for (const value of ${accessor}) {`);
294
+ output.push(
295
+ ` params.append("${param.name}", ${itemValueExpression})`
296
+ );
297
+ output.push(" }");
298
+ output.push(" }");
299
+ }
300
+ continue;
301
+ }
302
+ const valueExpression = convertQueryParamValue(schema, accessor);
303
+ if (param.required) {
304
+ output.push(` params.set("${param.name}", ${valueExpression})`);
305
+ } else {
306
+ output.push(` if (${accessor} !== undefined) {`);
307
+ output.push(` params.set("${param.name}", ${valueExpression})`);
308
+ output.push(" }");
309
+ }
250
310
  }
311
+ output.push(" const _searchParams = params.toString()");
312
+ output.push(
313
+ ` return \`${pathWithParams}\${_searchParams ? \`?\${_searchParams}\` : ""}\``
314
+ );
315
+ output.push(" },");
251
316
  }
252
317
  output.push("} as const;");
253
318
  output.push("");
@@ -339,6 +404,7 @@ function generate(spec, options = {}) {
339
404
  output.push("} as const;");
340
405
  output.push("");
341
406
  }
407
+ generateOperationTypes(output, operations, typesConfig);
342
408
  return output.join("\n");
343
409
  }
344
410
  function appendOperationField(buffer, key, value) {
@@ -374,11 +440,13 @@ function parseOperations(spec) {
374
440
  );
375
441
  const resolvedParameters = collectParameters(pathItem, operation, spec);
376
442
  const requestHeaders = getRequestHeaders(resolvedParameters);
443
+ const queryParams = getQueryParams(resolvedParameters);
377
444
  operations.push({
378
445
  operationId: operation.operationId,
379
446
  path: path2,
380
447
  method: method.toLowerCase(),
381
448
  pathParams,
449
+ queryParams,
382
450
  requestType,
383
451
  responseType: successResponse,
384
452
  requestHeaders,
@@ -388,6 +456,91 @@ function parseOperations(spec) {
388
456
  }
389
457
  return operations;
390
458
  }
459
+ function normalizeTypesConfig(config) {
460
+ return {
461
+ emit: config?.emit ?? true,
462
+ helpers: config?.helpers ?? "package",
463
+ helpersOutput: config?.helpersOutput ?? "./zenko-types"
464
+ };
465
+ }
466
+ function appendHelperTypesImport(buffer, config) {
467
+ if (!config.emit) return;
468
+ switch (config.helpers) {
469
+ case "package":
470
+ buffer.push(
471
+ 'import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "zenko";'
472
+ );
473
+ return;
474
+ case "file":
475
+ buffer.push(
476
+ `import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "${config.helpersOutput}";`
477
+ );
478
+ return;
479
+ case "inline":
480
+ buffer.push(
481
+ "type PathFn<TArgs extends unknown[] = []> = (...args: TArgs) => string;"
482
+ );
483
+ buffer.push(
484
+ "type HeaderFn<TArgs extends unknown[] = [], TResult = Record<string, unknown> | Record<string, never>> = (...args: TArgs) => TResult;"
485
+ );
486
+ buffer.push(
487
+ "type OperationErrors<TClient = unknown, TServer = unknown, TDefault = unknown, TOther = unknown> = {"
488
+ );
489
+ buffer.push(" clientErrors?: Record<string, TClient>;");
490
+ buffer.push(" serverErrors?: Record<string, TServer>;");
491
+ buffer.push(" defaultErrors?: Record<string, TDefault>;");
492
+ buffer.push(" otherErrors?: Record<string, TOther>;");
493
+ buffer.push("};");
494
+ buffer.push(
495
+ "type OperationDefinition<TPath extends (...args: any[]) => string, TRequest = undefined, TResponse = undefined, THeaders extends HeaderFn | undefined = undefined, TErrors extends OperationErrors | undefined = undefined> = {"
496
+ );
497
+ buffer.push(" path: TPath;");
498
+ buffer.push(" request?: TRequest;");
499
+ buffer.push(" response?: TResponse;");
500
+ buffer.push(" headers?: THeaders;");
501
+ buffer.push(" errors?: TErrors;");
502
+ buffer.push("};");
503
+ return;
504
+ }
505
+ }
506
+ function generateOperationTypes(buffer, operations, config) {
507
+ if (!config.emit) return;
508
+ buffer.push("// Operation Types");
509
+ for (const op of operations) {
510
+ const headerType = op.requestHeaders?.length ? `typeof headers.${op.operationId}` : "undefined";
511
+ const requestType = op.requestType ?? "undefined";
512
+ const responseType = op.responseType ?? "undefined";
513
+ const errorsType = buildOperationErrorsType(op.errors);
514
+ buffer.push(
515
+ `export type ${capitalize(op.operationId)}Operation = OperationDefinition<`
516
+ );
517
+ buffer.push(` typeof paths.${op.operationId},`);
518
+ buffer.push(` ${requestType},`);
519
+ buffer.push(` ${responseType},`);
520
+ buffer.push(` ${headerType},`);
521
+ buffer.push(` ${errorsType}`);
522
+ buffer.push(`>;`);
523
+ buffer.push("");
524
+ }
525
+ }
526
+ function buildOperationErrorsType(errors) {
527
+ if (!errors || !hasAnyErrors(errors)) {
528
+ return "OperationErrors";
529
+ }
530
+ const client = buildErrorBucket(errors.clientErrors);
531
+ const server = buildErrorBucket(errors.serverErrors);
532
+ const fallback = buildErrorBucket(errors.defaultErrors);
533
+ const other = buildErrorBucket(errors.otherErrors);
534
+ return `OperationErrors<${client}, ${server}, ${fallback}, ${other}>`;
535
+ }
536
+ function buildErrorBucket(bucket) {
537
+ if (!bucket || Object.keys(bucket).length === 0) {
538
+ return "unknown";
539
+ }
540
+ const entries = Object.entries(bucket);
541
+ const typeEntries = entries.map(([name, type]) => `${formatPropertyName(name)}: ${type}`).join("; ");
542
+ return `{ ${typeEntries} }`;
543
+ }
391
544
  function collectParameters(pathItem, operation, spec) {
392
545
  const parametersMap = /* @__PURE__ */ new Map();
393
546
  const addParameters = (params) => {
@@ -530,6 +683,20 @@ function getRequestHeaders(parameters) {
530
683
  }
531
684
  return headers;
532
685
  }
686
+ function getQueryParams(parameters) {
687
+ const queryParams = [];
688
+ for (const param of parameters ?? []) {
689
+ if (param.in === "query") {
690
+ queryParams.push({
691
+ name: param.name,
692
+ description: param.description,
693
+ schema: param.schema,
694
+ required: param.required
695
+ });
696
+ }
697
+ }
698
+ return queryParams;
699
+ }
533
700
  function mapHeaderType(header) {
534
701
  const schemaType = header.schema?.type;
535
702
  switch (schemaType) {
@@ -542,6 +709,39 @@ function mapHeaderType(header) {
542
709
  return "string";
543
710
  }
544
711
  }
712
+ function mapQueryType(param) {
713
+ return mapQuerySchemaType(param.schema);
714
+ }
715
+ function mapQuerySchemaType(schema) {
716
+ if (!schema) return "string";
717
+ if (schema.type === "array") {
718
+ const itemType = mapQuerySchemaType(schema.items);
719
+ return `Array<${itemType}>`;
720
+ }
721
+ switch (schema.type) {
722
+ case "integer":
723
+ case "number":
724
+ return "number";
725
+ case "boolean":
726
+ return "boolean";
727
+ default:
728
+ return "string";
729
+ }
730
+ }
731
+ function convertQueryParamValue(schema, accessor) {
732
+ if (!schema) {
733
+ return `String(${accessor})`;
734
+ }
735
+ switch (schema.type) {
736
+ case "integer":
737
+ case "number":
738
+ return `String(${accessor})`;
739
+ case "boolean":
740
+ return `${accessor} ? "true" : "false"`;
741
+ default:
742
+ return `String(${accessor})`;
743
+ }
744
+ }
545
745
  function generateZodSchema(name, schema, generatedTypes, options) {
546
746
  if (generatedTypes.has(name)) return "";
547
747
  generatedTypes.add(name);
@@ -550,18 +750,7 @@ function generateZodSchema(name, schema, generatedTypes, options) {
550
750
  return `export const ${name} = z.enum([${enumValues}]);`;
551
751
  }
552
752
  if (schema.type === "object" || schema.properties) {
553
- const properties = [];
554
- for (const [propName, propSchema] of Object.entries(
555
- schema.properties || {}
556
- )) {
557
- const isRequired = schema.required?.includes(propName) ?? false;
558
- const zodType = getZodTypeFromSchema(propSchema, options);
559
- const finalType = isRequired ? zodType : `${zodType}.optional()`;
560
- properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
561
- }
562
- return `export const ${name} = z.object({
563
- ${properties.join("\n")}
564
- });`;
753
+ return `export const ${name} = ${buildZodObject(schema, options)};`;
565
754
  }
566
755
  if (schema.type === "array") {
567
756
  const itemSchema = schema.items ?? { type: "unknown" };
@@ -584,6 +773,9 @@ function getZodTypeFromSchema(schema, options) {
584
773
  const enumValues = schema.enum.map((v) => `"${v}"`).join(", ");
585
774
  return `z.enum([${enumValues}])`;
586
775
  }
776
+ if (schema.type === "object" || schema.properties) {
777
+ return buildZodObject(schema, options);
778
+ }
587
779
  switch (schema.type) {
588
780
  case "string":
589
781
  return buildString(schema, options);
@@ -604,6 +796,23 @@ function getZodTypeFromSchema(schema, options) {
604
796
  return "z.unknown()";
605
797
  }
606
798
  }
799
+ function buildZodObject(schema, options) {
800
+ const properties = [];
801
+ for (const [propName, propSchema] of Object.entries(
802
+ schema.properties || {}
803
+ )) {
804
+ const isRequired = schema.required?.includes(propName) ?? false;
805
+ const zodType = getZodTypeFromSchema(propSchema, options);
806
+ const finalType = isRequired ? zodType : `${zodType}.optional()`;
807
+ properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
808
+ }
809
+ if (properties.length === 0) {
810
+ return "z.object({})";
811
+ }
812
+ return `z.object({
813
+ ${properties.join("\n")}
814
+ })`;
815
+ }
607
816
  function buildString(schema, options) {
608
817
  if (options.strictDates) {
609
818
  switch (schema.format) {
@@ -794,7 +1003,7 @@ function printHelp() {
794
1003
  console.log("");
795
1004
  console.log("Config file format:");
796
1005
  console.log(
797
- ' {"schemas": [{ input, output, strictDates?, strictNumeric? }] }'
1006
+ ' {"types"?: { emit?, helpers?, helpersOutput? }, "schemas": [{ input, output, strictDates?, strictNumeric?, types? }] }'
798
1007
  );
799
1008
  }
800
1009
  async function runFromConfig(parsed) {
@@ -803,14 +1012,17 @@ async function runFromConfig(parsed) {
803
1012
  const config = await loadConfig(resolvedConfigPath);
804
1013
  validateConfig(config);
805
1014
  const baseDir = path.dirname(resolvedConfigPath);
1015
+ const baseTypesConfig = config.types;
806
1016
  for (const entry of config.schemas) {
807
1017
  const inputFile = resolvePath(entry.input, baseDir);
808
1018
  const outputFile = resolvePath(entry.output, baseDir);
1019
+ const typesConfig = resolveTypesConfig(baseTypesConfig, entry.types);
809
1020
  await generateSingle({
810
1021
  inputFile,
811
1022
  outputFile,
812
1023
  strictDates: entry.strictDates ?? parsed.strictDates,
813
- strictNumeric: entry.strictNumeric ?? parsed.strictNumeric
1024
+ strictNumeric: entry.strictNumeric ?? parsed.strictNumeric,
1025
+ typesConfig
814
1026
  });
815
1027
  }
816
1028
  }
@@ -847,12 +1059,23 @@ function validateConfig(config) {
847
1059
  function resolvePath(filePath, baseDir) {
848
1060
  return path.isAbsolute(filePath) ? filePath : path.join(baseDir, filePath);
849
1061
  }
1062
+ function resolveTypesConfig(baseConfig, entryConfig) {
1063
+ if (!baseConfig && !entryConfig) return void 0;
1064
+ return {
1065
+ ...baseConfig,
1066
+ ...entryConfig
1067
+ };
1068
+ }
850
1069
  async function generateSingle(options) {
851
- const { inputFile, outputFile, strictDates, strictNumeric } = options;
1070
+ const { inputFile, outputFile, strictDates, strictNumeric, typesConfig } = options;
852
1071
  const resolvedInput = path.resolve(inputFile);
853
1072
  const resolvedOutput = path.resolve(outputFile);
854
1073
  const spec = readSpec(resolvedInput);
855
- const output = generate(spec, { strictDates, strictNumeric });
1074
+ const output = generate(spec, {
1075
+ strictDates,
1076
+ strictNumeric,
1077
+ types: typesConfig
1078
+ });
856
1079
  fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
857
1080
  fs.writeFileSync(resolvedOutput, output);
858
1081
  console.log(`\u2705 Generated TypeScript types in ${resolvedOutput}`);