@techspokes/typescript-wsdl-client 0.9.0 → 0.9.2

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.js CHANGED
@@ -26,10 +26,12 @@ import { emitClientArtifacts, handleCLIError, parseServers, parseStatusCodes, re
26
26
  import { buildCompilerOptionsFromArgv, buildOpenApiOptionsFromArgv } from "./util/builder.js";
27
27
  // Process command line arguments, removing the first two elements (node executable and script path)
28
28
  const rawArgs = hideBin(process.argv);
29
+ // Available commands
30
+ const COMMANDS = ["compile", "client", "openapi", "gateway", "app", "pipeline"];
29
31
  // ---------------------------------------------------------------------------
30
32
  // Show help if no subcommand provided
31
33
  // ---------------------------------------------------------------------------
32
- if (!rawArgs[0] || !["compile", "client", "openapi", "gateway", "pipeline"].includes(rawArgs[0])) {
34
+ if (!rawArgs[0] || !COMMANDS.includes(rawArgs[0])) {
33
35
  await yargs(rawArgs)
34
36
  .version(false)
35
37
  .scriptName("wsdl-tsc")
@@ -38,8 +40,9 @@ if (!rawArgs[0] || !["compile", "client", "openapi", "gateway", "pipeline"].incl
38
40
  .command("client", "Generate TypeScript SOAP client from WSDL or catalog")
39
41
  .command("openapi", "Generate OpenAPI specification from WSDL or catalog")
40
42
  .command("gateway", "Generate Fastify gateway from OpenAPI specification")
43
+ .command("app", "Generate runnable Fastify application from client + gateway + OpenAPI")
41
44
  .command("pipeline", "Run full generation pipeline (client + OpenAPI + gateway)")
42
- .demandCommand(1, "Please specify a command: compile, client, openapi, gateway, or pipeline")
45
+ .demandCommand(1, `Please specify a command: ${COMMANDS.join(", ")}`)
43
46
  .strict()
44
47
  .help()
45
48
  .parse();
@@ -389,6 +392,117 @@ if (rawArgs[0] === "gateway") {
389
392
  success(`Gateway code generated in ${outDir}`);
390
393
  process.exit(0);
391
394
  }
395
+ /**
396
+ * Command handler for "app" subcommand
397
+ *
398
+ * This branch handles the Fastify application generation mode, which creates a runnable
399
+ * Fastify server that uses the generated gateway plugin and SOAP client.
400
+ */
401
+ if (rawArgs[0] === "app") {
402
+ const { generateApp } = await import("./app/generateApp.js");
403
+ const appArgv = await yargs(rawArgs.slice(1))
404
+ .version(false)
405
+ .scriptName("wsdl-tsc app")
406
+ .usage("$0 --client-dir <dir> --gateway-dir <dir> --openapi-file <file> [--catalog-file <file>] [--app-dir <dir>] [options]")
407
+ .option("client-dir", {
408
+ type: "string",
409
+ demandOption: true,
410
+ desc: "Path to client directory (where client.ts is located)"
411
+ })
412
+ .option("gateway-dir", {
413
+ type: "string",
414
+ demandOption: true,
415
+ desc: "Path to gateway directory (where plugin.ts is located)"
416
+ })
417
+ .option("openapi-file", {
418
+ type: "string",
419
+ demandOption: true,
420
+ desc: "Path to OpenAPI spec file"
421
+ })
422
+ .option("catalog-file", {
423
+ type: "string",
424
+ desc: "Path to catalog.json (default: {client-dir}/catalog.json)"
425
+ })
426
+ .option("app-dir", {
427
+ type: "string",
428
+ desc: "Output directory for generated app (default: sibling to gateway-dir, named 'app')"
429
+ })
430
+ .option("import-extensions", {
431
+ type: "string",
432
+ choices: ["js", "ts", "bare"],
433
+ desc: "Import-extension mode for generated TypeScript modules (inferred from catalog when available)"
434
+ })
435
+ .option("host", {
436
+ type: "string",
437
+ default: "127.0.0.1",
438
+ desc: "Default server host"
439
+ })
440
+ .option("port", {
441
+ type: "number",
442
+ default: 3000,
443
+ desc: "Default server port"
444
+ })
445
+ .option("prefix", {
446
+ type: "string",
447
+ default: "",
448
+ desc: "Route prefix"
449
+ })
450
+ .option("logger", {
451
+ type: "boolean",
452
+ default: true,
453
+ desc: "Enable Fastify logger"
454
+ })
455
+ .option("openapi-mode", {
456
+ type: "string",
457
+ choices: ["copy", "reference"],
458
+ default: "copy",
459
+ desc: "How to handle OpenAPI file: copy into app dir or reference original"
460
+ })
461
+ .strict()
462
+ .help()
463
+ .parse();
464
+ const clientDir = path.resolve(String(appArgv["client-dir"]));
465
+ const gatewayDir = path.resolve(String(appArgv["gateway-dir"]));
466
+ const openapiFile = path.resolve(String(appArgv["openapi-file"]));
467
+ // Determine catalog file path
468
+ const catalogFile = appArgv["catalog-file"]
469
+ ? path.resolve(String(appArgv["catalog-file"]))
470
+ : path.join(clientDir, "catalog.json");
471
+ // Determine app directory (default: sibling to gateway-dir, named 'app')
472
+ const appDir = appArgv["app-dir"]
473
+ ? path.resolve(String(appArgv["app-dir"]))
474
+ : path.join(path.dirname(gatewayDir), "app");
475
+ // Infer import-extensions from catalog if not explicitly provided
476
+ let imports = appArgv["import-extensions"];
477
+ if (!imports && fs.existsSync(catalogFile)) {
478
+ try {
479
+ const catalogContent = fs.readFileSync(catalogFile, "utf-8");
480
+ const catalog = JSON.parse(catalogContent);
481
+ imports = catalog?.options?.imports || "js";
482
+ }
483
+ catch (err) {
484
+ // If catalog read fails, fall back to "js"
485
+ imports = "js";
486
+ }
487
+ }
488
+ else if (!imports) {
489
+ imports = "js";
490
+ }
491
+ await generateApp({
492
+ clientDir,
493
+ gatewayDir,
494
+ openapiFile,
495
+ catalogFile,
496
+ appDir,
497
+ imports,
498
+ host: String(appArgv.host),
499
+ port: Number(appArgv.port),
500
+ prefix: String(appArgv.prefix),
501
+ logger: Boolean(appArgv.logger),
502
+ openapiMode: appArgv["openapi-mode"],
503
+ });
504
+ process.exit(0);
505
+ }
392
506
  if (rawArgs[0] === "pipeline") {
393
507
  const pipelineArgv = await yargs(rawArgs.slice(1))
394
508
  .version(false)
@@ -487,6 +601,22 @@ if (rawArgs[0] === "pipeline") {
487
601
  type: "boolean",
488
602
  default: false,
489
603
  desc: "Skip generating runtime.ts utilities"
604
+ })
605
+ // App generation flags
606
+ .option("generate-app", {
607
+ type: "boolean",
608
+ default: false,
609
+ desc: "Generate runnable Fastify application (requires --client-dir, --gateway-dir, --openapi-file)"
610
+ })
611
+ .option("app-dir", {
612
+ type: "string",
613
+ desc: "Output directory for generated app (default: sibling to gateway-dir, named 'app')"
614
+ })
615
+ .option("app-openapi-mode", {
616
+ type: "string",
617
+ choices: ["copy", "reference"],
618
+ default: "copy",
619
+ desc: "How to handle OpenAPI file in app: copy or reference"
490
620
  })
491
621
  .strict()
492
622
  .help()
@@ -550,6 +680,13 @@ if (rawArgs[0] === "pipeline") {
550
680
  const openApiOptions = openapiOut
551
681
  ? buildOpenApiOptionsFromArgv(pipelineArgv, format, servers)
552
682
  : undefined;
683
+ // Validate app generation requirements
684
+ const generateApp = pipelineArgv["generate-app"];
685
+ if (generateApp) {
686
+ if (!clientOut || !gatewayOut || !openapiOut) {
687
+ handleCLIError("--generate-app requires --client-dir, --gateway-dir, and --openapi-file to be set");
688
+ }
689
+ }
553
690
  await runGenerationPipeline({
554
691
  wsdl: pipelineArgv["wsdl-source"],
555
692
  clientOutDir: clientOut ? path.resolve(clientOut) : undefined,
@@ -570,6 +707,12 @@ if (rawArgs[0] === "pipeline") {
570
707
  emitPlugin: pipelineArgv["gateway-skip-plugin"] ? false : undefined,
571
708
  emitRuntime: pipelineArgv["gateway-skip-runtime"] ? false : undefined,
572
709
  } : undefined,
710
+ app: generateApp ? {
711
+ appDir: pipelineArgv["app-dir"]
712
+ ? path.resolve(pipelineArgv["app-dir"])
713
+ : path.join(path.dirname(path.resolve(gatewayOut)), "app"),
714
+ openapiMode: pipelineArgv["app-openapi-mode"],
715
+ } : undefined,
573
716
  });
574
717
  process.exit(0);
575
718
  }
@@ -1 +1 @@
1
- {"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAA+B,MAAM,cAAc,CAAC;AAEjG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,0BAA0B,EAAE,MAAM,EAAE,EACpC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,GAAG,EACvH,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,EACnC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACnC,iBAAiB,EAAE,CA2HrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAsBN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA2CN;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAkLN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CAyFN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,IAAI,CAwEN"}
1
+ {"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAA6C,MAAM,cAAc,CAAC;AAE/G;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,0BAA0B,EAAE,MAAM,EAAE,EACpC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,GAAG,EACvH,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,EACnC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACnC,iBAAiB,EAAE,CA2HrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAsBN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA2CN;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAkLN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CAyFN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,IAAI,CAwEN"}
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
- import { rewriteSchemaRefs, slugName, } from "./helpers.js";
15
+ import { flattenAllOf, rewriteSchemaRefs, slugName, } from "./helpers.js";
16
16
  /**
17
17
  * Emits individual JSON Schema files for each OpenAPI component schema
18
18
  *
@@ -42,10 +42,13 @@ export function emitModelSchemas(schemas, modelsDir, versionSlug, serviceSlug) {
42
42
  modelSlugByName[name] = slug;
43
43
  schemaIdByName[name] = `urn:services:${serviceSlug}:${versionSlug}:schemas:models:${slug}`;
44
44
  }
45
- // Second pass: rewrite refs and emit files
45
+ // Second pass: flatten allOf, rewrite refs, and emit files
46
46
  for (const [name, schema] of Object.entries(schemas)) {
47
47
  const slug = modelSlugByName[name];
48
- const cloned = rewriteSchemaRefs(schema, schemaIdByName);
48
+ // Flatten allOf compositions for Fastify/fast-json-stringify compatibility
49
+ const flattened = flattenAllOf(schema, schemas);
50
+ // Rewrite $refs to URN format
51
+ const cloned = rewriteSchemaRefs(flattened, schemaIdByName);
49
52
  cloned.$id = schemaIdByName[name];
50
53
  const outPath = path.join(modelsDir, `${slug}.json`);
51
54
  fs.writeFileSync(outPath, JSON.stringify(cloned, null, 2), "utf8");
@@ -61,6 +61,24 @@ export declare function isNumericStatus(code: string): boolean;
61
61
  * @throws {Error} If unknown schema reference is encountered
62
62
  */
63
63
  export declare function rewriteSchemaRefs(node: any, schemaIdByName: Record<string, string>): any;
64
+ /**
65
+ * Flattens allOf compositions into a single merged schema for Fastify compatibility.
66
+ *
67
+ * This is necessary because fast-json-stringify cannot handle deeply nested allOf
68
+ * compositions, especially when referenced schemas also use allOf for inheritance.
69
+ *
70
+ * The function:
71
+ * - Recursively resolves $ref references within allOf members
72
+ * - Merges properties, required arrays, and other keywords
73
+ * - Handles circular references by keeping them as $ref
74
+ * - Preserves anyOf/oneOf without flattening (only allOf is problematic)
75
+ *
76
+ * @param {any} schema - The schema to flatten
77
+ * @param {Record<string, any>} allSchemas - All available schemas for $ref resolution
78
+ * @param {Set<string>} [visited] - Set of visited schema names to detect circular references
79
+ * @returns {any} - Flattened schema without allOf (or original if no allOf present)
80
+ */
81
+ export declare function flattenAllOf(schema: any, allSchemas: Record<string, any>, visited?: Set<string>): any;
64
82
  /**
65
83
  * Extracts the component schema name from a $ref object
66
84
  *
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/gateway/helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/gateway/helpers.ts"],"names":[],"mappings":"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"}
@@ -77,6 +77,123 @@ export function rewriteSchemaRefs(node, schemaIdByName) {
77
77
  }
78
78
  return node;
79
79
  }
80
+ /**
81
+ * Flattens allOf compositions into a single merged schema for Fastify compatibility.
82
+ *
83
+ * This is necessary because fast-json-stringify cannot handle deeply nested allOf
84
+ * compositions, especially when referenced schemas also use allOf for inheritance.
85
+ *
86
+ * The function:
87
+ * - Recursively resolves $ref references within allOf members
88
+ * - Merges properties, required arrays, and other keywords
89
+ * - Handles circular references by keeping them as $ref
90
+ * - Preserves anyOf/oneOf without flattening (only allOf is problematic)
91
+ *
92
+ * @param {any} schema - The schema to flatten
93
+ * @param {Record<string, any>} allSchemas - All available schemas for $ref resolution
94
+ * @param {Set<string>} [visited] - Set of visited schema names to detect circular references
95
+ * @returns {any} - Flattened schema without allOf (or original if no allOf present)
96
+ */
97
+ export function flattenAllOf(schema, allSchemas, visited = new Set()) {
98
+ if (!schema || typeof schema !== "object") {
99
+ return schema;
100
+ }
101
+ // Handle arrays
102
+ if (Array.isArray(schema)) {
103
+ return schema.map(item => flattenAllOf(item, allSchemas, visited));
104
+ }
105
+ // If no allOf, recursively process nested schemas but preserve structure
106
+ if (!schema.allOf) {
107
+ const result = {};
108
+ for (const [key, value] of Object.entries(schema)) {
109
+ if (key === "properties" && value && typeof value === "object") {
110
+ result[key] = {};
111
+ for (const [propName, propSchema] of Object.entries(value)) {
112
+ result[key][propName] = flattenAllOf(propSchema, allSchemas, visited);
113
+ }
114
+ }
115
+ else if (key === "items" && value && typeof value === "object") {
116
+ result[key] = flattenAllOf(value, allSchemas, visited);
117
+ }
118
+ else if ((key === "anyOf" || key === "oneOf") && Array.isArray(value)) {
119
+ result[key] = value.map(v => flattenAllOf(v, allSchemas, visited));
120
+ }
121
+ else if (key === "additionalProperties" && value && typeof value === "object") {
122
+ result[key] = flattenAllOf(value, allSchemas, visited);
123
+ }
124
+ else {
125
+ result[key] = value;
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+ // Flatten allOf: merge all schemas into one
131
+ const merged = {
132
+ type: "object",
133
+ properties: {},
134
+ };
135
+ const requiredSet = new Set();
136
+ for (const member of schema.allOf) {
137
+ let resolvedMember = member;
138
+ // Resolve $ref if present
139
+ if (member.$ref && typeof member.$ref === "string") {
140
+ const refPath = member.$ref;
141
+ let schemaName = null;
142
+ if (refPath.startsWith("#/components/schemas/")) {
143
+ schemaName = refPath.slice("#/components/schemas/".length);
144
+ }
145
+ if (schemaName && allSchemas[schemaName]) {
146
+ if (visited.has(schemaName)) {
147
+ // Circular reference - keep the $ref as-is in properties that reference it
148
+ continue;
149
+ }
150
+ const newVisited = new Set(visited);
151
+ newVisited.add(schemaName);
152
+ resolvedMember = flattenAllOf(allSchemas[schemaName], allSchemas, newVisited);
153
+ }
154
+ else {
155
+ // Unknown ref - keep as-is (will be rewritten later by rewriteSchemaRefs)
156
+ continue;
157
+ }
158
+ }
159
+ // Merge properties
160
+ if (resolvedMember.properties && typeof resolvedMember.properties === "object") {
161
+ for (const [propName, propSchema] of Object.entries(resolvedMember.properties)) {
162
+ merged.properties[propName] = flattenAllOf(propSchema, allSchemas, visited);
163
+ }
164
+ }
165
+ // Merge required
166
+ if (Array.isArray(resolvedMember.required)) {
167
+ for (const req of resolvedMember.required) {
168
+ requiredSet.add(req);
169
+ }
170
+ }
171
+ // Copy other keywords (last one wins for conflicts)
172
+ if (resolvedMember.additionalProperties !== undefined) {
173
+ merged.additionalProperties = resolvedMember.additionalProperties;
174
+ }
175
+ if (resolvedMember.description !== undefined) {
176
+ merged.description = resolvedMember.description;
177
+ }
178
+ if (resolvedMember.title !== undefined) {
179
+ merged.title = resolvedMember.title;
180
+ }
181
+ }
182
+ // Apply description from the original allOf schema if present (overrides merged)
183
+ if (schema.description !== undefined) {
184
+ merged.description = schema.description;
185
+ }
186
+ // Convert required set to array
187
+ if (requiredSet.size > 0) {
188
+ merged.required = Array.from(requiredSet).sort();
189
+ }
190
+ // Clean up empty properties object
191
+ if (Object.keys(merged.properties).length === 0) {
192
+ delete merged.properties;
193
+ delete merged.type; // No properties means we shouldn't force type: object
194
+ }
195
+ return merged;
196
+ }
80
197
  /**
81
198
  * Extracts the component schema name from a $ref object
82
199
  *
@@ -126,8 +126,7 @@ function buildComplexSchema(t, closed, knownTypeNames, aliasNames) {
126
126
  obj.allOf = [{ $ref: `#/components/schemas/${baseName}` }, { ...obj }];
127
127
  delete obj.type; // inner object part handled in allOf
128
128
  delete obj.properties;
129
- if (!required.length)
130
- delete obj.required;
129
+ delete obj.required; // Always remove from top-level; it's already in the allOf member
131
130
  if (!closed)
132
131
  delete obj.additionalProperties; // put closed only on leaf part
133
132
  }
@@ -15,6 +15,9 @@ import { type CompilerOptions } from "./config.js";
15
15
  * @property {string} gateway.outDir - Output directory for gateway code
16
16
  * @property {string} gateway.versionSlug - Version identifier for URN generation (required)
17
17
  * @property {string} gateway.serviceSlug - Service identifier for URN generation (required)
18
+ * @property {object} [app] - App generation configuration (optional, requires client, gateway, and openapi)
19
+ * @property {string} app.appDir - Output directory for generated Fastify app
20
+ * @property {"copy"|"reference"} [app.openapiMode] - How to handle OpenAPI file (default: "copy")
18
21
  */
19
22
  export interface PipelineOptions {
20
23
  wsdl: string;
@@ -29,9 +32,13 @@ export interface PipelineOptions {
29
32
  versionSlug: string;
30
33
  serviceSlug: string;
31
34
  };
35
+ app?: {
36
+ appDir: string;
37
+ openapiMode?: "copy" | "reference";
38
+ };
32
39
  }
33
40
  /**
34
- * Runs the complete generation pipeline from WSDL to TypeScript artifacts and optionally OpenAPI/Gateway
41
+ * Runs the complete generation pipeline from WSDL to TypeScript artifacts and optionally OpenAPI/Gateway/App
35
42
  *
36
43
  * This function orchestrates the entire process:
37
44
  * 1. Loads and parses the WSDL from file or URL
@@ -40,6 +47,7 @@ export interface PipelineOptions {
40
47
  * 4. Optionally emits a JSON catalog for introspection
41
48
  * 5. Optionally generates an OpenAPI 3.1 specification
42
49
  * 6. Optionally generates Fastify gateway code from the OpenAPI spec
50
+ * 7. Optionally generates a runnable Fastify app that uses client and gateway
43
51
  *
44
52
  * @param {PipelineOptions} opts - Configuration options for the pipeline
45
53
  * @returns {Promise<{compiled: any; openapiDoc?: any}>} - The compiled catalog and optional OpenAPI document
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAC,KAAK,eAAe,EAAyB,MAAM,aAAa,CAAC;AAGzE;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1G,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,CAAC;CAAE,CAAC,CA+FhH"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAC,KAAK,eAAe,EAAyB,MAAM,aAAa,CAAC;AAGzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1G,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;KACpC,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,CAAC;CAAE,CAAC,CAmHhH"}
package/dist/pipeline.js CHANGED
@@ -19,7 +19,7 @@ import { generateGateway } from "./gateway/generateGateway.js";
19
19
  import { resolveCompilerOptions } from "./config.js";
20
20
  import { emitClientArtifacts, reportCompilationStats, reportOpenApiSuccess, success } from "./util/cli.js";
21
21
  /**
22
- * Runs the complete generation pipeline from WSDL to TypeScript artifacts and optionally OpenAPI/Gateway
22
+ * Runs the complete generation pipeline from WSDL to TypeScript artifacts and optionally OpenAPI/Gateway/App
23
23
  *
24
24
  * This function orchestrates the entire process:
25
25
  * 1. Loads and parses the WSDL from file or URL
@@ -28,6 +28,7 @@ import { emitClientArtifacts, reportCompilationStats, reportOpenApiSuccess, succ
28
28
  * 4. Optionally emits a JSON catalog for introspection
29
29
  * 5. Optionally generates an OpenAPI 3.1 specification
30
30
  * 6. Optionally generates Fastify gateway code from the OpenAPI spec
31
+ * 7. Optionally generates a runnable Fastify app that uses client and gateway
31
32
  *
32
33
  * @param {PipelineOptions} opts - Configuration options for the pipeline
33
34
  * @returns {Promise<{compiled: any; openapiDoc?: any}>} - The compiled catalog and optional OpenAPI document
@@ -101,6 +102,23 @@ export async function runGenerationPipeline(opts) {
101
102
  });
102
103
  success(`Gateway code generated in ${gatewayOutDir}`);
103
104
  }
105
+ // Step 6: Optionally generate Fastify app
106
+ if (opts.app) {
107
+ // App generation requires client, gateway, and openapi to be generated
108
+ if (!opts.clientOutDir || !opts.gateway?.outDir || !opts.openapi?.outFile) {
109
+ throw new Error("App generation requires client, gateway, and OpenAPI to be generated in the pipeline");
110
+ }
111
+ const { generateApp } = await import("./app/generateApp.js");
112
+ await generateApp({
113
+ clientDir: path.resolve(opts.clientOutDir),
114
+ gatewayDir: path.resolve(opts.gateway.outDir),
115
+ openapiFile: path.resolve(opts.openapi.outFile),
116
+ catalogFile: path.resolve(opts.catalogOut),
117
+ appDir: path.resolve(opts.app.appDir),
118
+ imports: finalCompiler.imports,
119
+ openapiMode: opts.app.openapiMode || "copy",
120
+ });
121
+ }
104
122
  // Return the compiled catalog and OpenAPI doc for potential further processing
105
123
  return { compiled, openapiDoc };
106
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "TypeScript WSDL → SOAP client generator with full xs:attribute support, complex types, sequences, inheritance, and namespace-collision merging.",
5
5
  "keywords": [
6
6
  "wsdl",
@@ -57,8 +57,9 @@
57
57
  "smoke:client": "rimraf tmp && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsc -p tsconfig.smoke.json",
58
58
  "smoke:openapi": "rimraf tmp && tsx src/cli.ts openapi --wsdl-source examples/minimal/weather.wsdl --openapi-file tmp/openapi.json --openapi-format json && tsc -p tsconfig.smoke.json",
59
59
  "smoke:gateway": "rimraf tmp && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsx src/cli.ts openapi --catalog-file tmp/client/catalog.json --openapi-file tmp/openapi.json --openapi-format json && tsx src/cli.ts gateway --openapi-file tmp/openapi.json --client-dir tmp/client --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 && tsc -p tsconfig.smoke.json",
60
+ "smoke:app": "rimraf tmp && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api --generate-app && tsc -p tsconfig.smoke.json",
60
61
  "smoke:pipeline": "rimraf tmp && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api && tsc -p tsconfig.smoke.json",
61
- "ci": "npm run build && npm run typecheck && npm run smoke:pipeline"
62
+ "ci": "npm run build && npm run typecheck && npm run smoke:app"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@types/js-yaml": "^4.0.9",