@techspokes/typescript-wsdl-client 0.9.1 → 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.
@@ -0,0 +1,553 @@
1
+ // noinspection HttpUrlsUsage
2
+ /**
3
+ * Fastify App Generator
4
+ *
5
+ * This module generates a runnable Fastify application that imports and uses
6
+ * the generated gateway plugin and SOAP client. The app serves the OpenAPI spec,
7
+ * health checks, and all gateway routes.
8
+ *
9
+ * Core capabilities:
10
+ * - Generates server.ts with Fastify setup and plugin registration
11
+ * - Generates config.ts with environment-based configuration
12
+ * - Generates .env.example with required/optional environment variables
13
+ * - Generates README.md with instructions for running the app
14
+ * - Optionally copies OpenAPI spec into app directory
15
+ * - Validates required inputs (client-dir, gateway-dir, openapi-file, catalog-file)
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { deriveClientName } from "../util/tools.js";
20
+ import { success } from "../util/cli.js";
21
+ /**
22
+ * Validates that all required files and directories exist
23
+ *
24
+ * @param {GenerateAppOptions} opts - App generation options
25
+ * @throws {Error} If any required file or directory doesn't exist
26
+ */
27
+ function validateRequiredFiles(opts) {
28
+ const checks = [
29
+ { path: opts.clientDir, type: "directory", label: "Client directory" },
30
+ { path: opts.gatewayDir, type: "directory", label: "Gateway directory" },
31
+ { path: opts.catalogFile, type: "file", label: "Catalog file" },
32
+ { path: opts.openapiFile, type: "file", label: "OpenAPI file" },
33
+ ];
34
+ for (const check of checks) {
35
+ const exists = fs.existsSync(check.path);
36
+ if (!exists) {
37
+ throw new Error(`${check.label} does not exist: ${check.path}`);
38
+ }
39
+ const stat = fs.statSync(check.path);
40
+ if (check.type === "directory" && !stat.isDirectory()) {
41
+ throw new Error(`${check.label} is not a directory: ${check.path}`);
42
+ }
43
+ if (check.type === "file" && !stat.isFile()) {
44
+ throw new Error(`${check.label} is not a file: ${check.path}`);
45
+ }
46
+ }
47
+ // Check for client entrypoint (try multiple extensions)
48
+ const clientEntrypointFound = ["", ".ts", ".js"].some(ext => {
49
+ return fs.existsSync(path.join(opts.clientDir, `client${ext}`));
50
+ });
51
+ if (!clientEntrypointFound) {
52
+ throw new Error(`Client entrypoint does not exist in ${opts.clientDir} (tried client.ts, client.js, client)`);
53
+ }
54
+ // Check for gateway plugin entrypoint (try multiple extensions)
55
+ const gatewayEntrypointFound = ["", ".ts", ".js"].some(ext => {
56
+ return fs.existsSync(path.join(opts.gatewayDir, `plugin${ext}`));
57
+ });
58
+ if (!gatewayEntrypointFound) {
59
+ throw new Error(`Gateway plugin entrypoint does not exist in ${opts.gatewayDir} (tried plugin.ts, plugin.js, plugin)`);
60
+ }
61
+ }
62
+ /**
63
+ * Returns the file extension for the import mode
64
+ *
65
+ * @param {string} imports - Import mode (js, ts, or bare)
66
+ * @returns {string} - File extension with leading dot or empty string for bare
67
+ */
68
+ function getExtension(imports) {
69
+ if (imports === "js")
70
+ return ".js";
71
+ if (imports === "ts")
72
+ return ".ts";
73
+ return "";
74
+ }
75
+ /**
76
+ * Returns the file extension for executable app files (server, config)
77
+ * These always need extensions even with bare imports since they are entry points
78
+ *
79
+ * @param {string} imports - Import mode (js, ts, or bare)
80
+ * @returns {string} - File extension with leading dot
81
+ */
82
+ function getAppFileExtension(imports) {
83
+ if (imports === "ts")
84
+ return ".ts";
85
+ return ".js"; // Use .js for both "js" and "bare" modes
86
+ }
87
+ /**
88
+ * Computes a relative import path from source to target
89
+ *
90
+ * @param {string} from - Source directory
91
+ * @param {string} to - Target file or directory
92
+ * @param {string} imports - Import mode (js, ts, or bare)
93
+ * @returns {string} - Relative import specifier with proper extension
94
+ */
95
+ function computeRelativeImport(from, to, imports) {
96
+ const rel = path.relative(from, to);
97
+ // Normalize to POSIX separators
98
+ const posix = rel.split(path.sep).join("/");
99
+ // Ensure it starts with ./ or ../
100
+ const prefixed = posix.startsWith(".") ? posix : `./${posix}`;
101
+ // Apply import extension rules
102
+ const ext = getExtension(imports);
103
+ if (ext) {
104
+ return prefixed + ext;
105
+ }
106
+ return prefixed;
107
+ }
108
+ /**
109
+ * Reads and parses the catalog file
110
+ *
111
+ * @param {string} catalogPath - Path to catalog.json
112
+ * @returns {any} - Parsed catalog object
113
+ */
114
+ function readCatalog(catalogPath) {
115
+ try {
116
+ const content = fs.readFileSync(catalogPath, "utf-8");
117
+ return JSON.parse(content);
118
+ }
119
+ catch (err) {
120
+ throw new Error(`Failed to read or parse catalog file: ${err instanceof Error ? err.message : String(err)}`);
121
+ }
122
+ }
123
+ /**
124
+ * Derives default WSDL source from catalog if available
125
+ *
126
+ * @param {any} catalog - Parsed catalog object
127
+ * @returns {string|undefined} - WSDL source from catalog or undefined
128
+ */
129
+ function getCatalogWsdlSource(catalog) {
130
+ return catalog?.options?.wsdl || catalog?.wsdlUri;
131
+ }
132
+ /**
133
+ * Generates server.ts file
134
+ *
135
+ * @param {string} appDir - App output directory
136
+ * @param {GenerateAppOptions} opts - App generation options
137
+ * @param {string} clientClassName - Derived client class name
138
+ */
139
+ function generateServerFile(appDir, opts, clientClassName) {
140
+ const imports = opts.imports || "js";
141
+ const ext = getAppFileExtension(imports); // Use getAppFileExtension for executable entry point
142
+ const configImport = computeRelativeImport(appDir, path.join(appDir, "config"), imports);
143
+ const gatewayPluginImport = computeRelativeImport(appDir, path.join(opts.gatewayDir, "plugin"), imports);
144
+ const clientImport = computeRelativeImport(appDir, path.join(opts.clientDir, "client"), imports);
145
+ // For OpenAPI serving, we need to read and parse the file at startup in ESM mode
146
+ const openapiServeLogic = opts.openapiMode === "copy"
147
+ ? `
148
+ // Read and parse OpenAPI spec at startup
149
+ const openapiSpecPath = path.join(__dirname, "openapi.json");
150
+ const openapiSpec = JSON.parse(fs.readFileSync(openapiSpecPath, "utf-8"));
151
+
152
+ // Serve OpenAPI specification
153
+ fastify.get("/openapi.json", async () => {
154
+ return openapiSpec;
155
+ });`
156
+ : `
157
+ // Serve OpenAPI specification from original file
158
+ const openapiSpecPath = path.resolve(__dirname, "${computeRelativeImport(appDir, opts.openapiFile, "bare")}");
159
+ const openapiSpec = JSON.parse(fs.readFileSync(openapiSpecPath, "utf-8"));
160
+
161
+ fastify.get("/openapi.json", async () => {
162
+ return openapiSpec;
163
+ });`;
164
+ const content = `/**
165
+ * Generated Fastify Application
166
+ *
167
+ * This file bootstraps a Fastify server that:
168
+ * - Loads configuration from environment variables
169
+ * - Instantiates the SOAP client
170
+ * - Registers the gateway plugin
171
+ * - Serves the OpenAPI specification
172
+ * - Provides health check endpoint
173
+ *
174
+ * Auto-generated - do not edit manually.
175
+ */
176
+ import fs from "node:fs";
177
+ import path from "node:path";
178
+ import { fileURLToPath } from "node:url";
179
+ import { dirname } from "node:path";
180
+ import Fastify from "fastify";
181
+ import { loadConfig } from "${configImport}";
182
+ import gatewayPlugin from "${gatewayPluginImport}";
183
+ import { ${clientClassName} } from "${clientImport}";
184
+
185
+ // ES module dirname/filename helpers
186
+ const __filename = fileURLToPath(import.meta.url);
187
+ const __dirname = dirname(__filename);
188
+
189
+ /**
190
+ * Main application entry point
191
+ */
192
+ async function main() {
193
+ // Load configuration from environment
194
+ const config = loadConfig();
195
+
196
+ // Create Fastify instance
197
+ const fastify = Fastify({
198
+ logger: config.logger,
199
+ });
200
+
201
+ // Instantiate SOAP client
202
+ const client = new ${clientClassName}({
203
+ source: config.wsdlSource,
204
+ });
205
+
206
+ // Register gateway plugin
207
+ await fastify.register(gatewayPlugin, {
208
+ client,
209
+ prefix: config.prefix,
210
+ });
211
+
212
+ // Health check endpoint
213
+ fastify.get("/health", async () => {
214
+ return { ok: true };
215
+ });
216
+ ${openapiServeLogic}
217
+
218
+ // Start server
219
+ try {
220
+ await fastify.listen({
221
+ host: config.host,
222
+ port: config.port,
223
+ });
224
+ fastify.log.info(\`Server listening on http://\${config.host}:\${config.port}\`);
225
+ } catch (err) {
226
+ fastify.log.error(err);
227
+ process.exit(1);
228
+ }
229
+ }
230
+
231
+ // Handle shutdown gracefully
232
+ process.on("SIGINT", () => {
233
+ console.log("\\nReceived SIGINT, shutting down gracefully...");
234
+ process.exit(0);
235
+ });
236
+
237
+ process.on("SIGTERM", () => {
238
+ console.log("\\nReceived SIGTERM, shutting down gracefully...");
239
+ process.exit(0);
240
+ });
241
+
242
+ // Run the application
243
+ main().catch((err) => {
244
+ console.error("Failed to start application:", err);
245
+ process.exit(1);
246
+ });
247
+ `;
248
+ fs.writeFileSync(path.join(appDir, `server${ext}`), content, "utf-8");
249
+ }
250
+ /**
251
+ * Generates config.ts file
252
+ *
253
+ * @param {string} appDir - App output directory
254
+ * @param {GenerateAppOptions} opts - App generation options
255
+ * @param {string|undefined} defaultWsdlSource - Default WSDL source from catalog
256
+ */
257
+ function generateConfigFile(appDir, opts, defaultWsdlSource) {
258
+ const imports = opts.imports || "js";
259
+ const ext = getAppFileExtension(imports); // Use getAppFileExtension for executable entry point
260
+ const defaultHost = opts.host || "127.0.0.1";
261
+ const defaultPort = opts.port || 3000;
262
+ const defaultPrefix = opts.prefix || "";
263
+ const defaultLogger = opts.logger !== false;
264
+ // For .js files, we need to generate plain JavaScript (no TypeScript types)
265
+ // For .ts files, we can use TypeScript syntax
266
+ const isTypeScript = ext === ".ts";
267
+ const typeAnnotations = isTypeScript ? `
268
+ /**
269
+ * Application configuration interface
270
+ */
271
+ export interface AppConfig {
272
+ wsdlSource: string;
273
+ host: string;
274
+ port: number;
275
+ prefix: string;
276
+ logger: boolean;
277
+ }
278
+
279
+ /**
280
+ * Loads configuration from environment variables
281
+ *
282
+ * @returns {AppConfig} - Application configuration
283
+ * @throws {Error} If required configuration is missing
284
+ */` : `
285
+ /**
286
+ * Loads configuration from environment variables
287
+ *
288
+ * @returns {object} - Application configuration with wsdlSource, host, port, prefix, logger
289
+ * @throws {Error} If required configuration is missing
290
+ */`;
291
+ const content = `/**
292
+ * Application Configuration
293
+ *
294
+ * Loads configuration from environment variables with sensible defaults.
295
+ * Configuration precedence:
296
+ * 1. Environment variables (runtime overrides)
297
+ * 2. Catalog defaults (generation-time recorded values)
298
+ * 3. Hard defaults (defined in this file)
299
+ *
300
+ * Auto-generated - do not edit manually.
301
+ */
302
+ ${typeAnnotations}
303
+ export function loadConfig() {
304
+ // WSDL source: required from env or catalog default
305
+ const wsdlSource = process.env.WSDL_SOURCE${defaultWsdlSource ? ` || "${defaultWsdlSource}"` : ""};
306
+ if (!wsdlSource) {
307
+ throw new Error("WSDL_SOURCE environment variable is required");
308
+ }
309
+
310
+ // Host: default to ${defaultHost}
311
+ const host = process.env.HOST || "${defaultHost}";
312
+
313
+ // Port: default to ${defaultPort}
314
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : ${defaultPort};
315
+ if (isNaN(port) || port < 1 || port > 65535) {
316
+ throw new Error(\`Invalid PORT value: \${process.env.PORT}\`);
317
+ }
318
+
319
+ // Prefix: default to empty string
320
+ const prefix = process.env.PREFIX || "${defaultPrefix}";
321
+
322
+ // Logger: default to ${defaultLogger}
323
+ const logger = process.env.LOGGER ? process.env.LOGGER === "true" : ${defaultLogger};
324
+
325
+ return {
326
+ wsdlSource,
327
+ host,
328
+ port,
329
+ prefix,
330
+ logger,
331
+ };
332
+ }
333
+ `;
334
+ fs.writeFileSync(path.join(appDir, `config${ext}`), content, "utf-8");
335
+ }
336
+ /**
337
+ * Generates .env.example file
338
+ *
339
+ * @param {string} appDir - App output directory
340
+ * @param {GenerateAppOptions} opts - App generation options
341
+ * @param {string|undefined} defaultWsdlSource - Default WSDL source from catalog
342
+ */
343
+ function generateEnvExample(appDir, opts, defaultWsdlSource) {
344
+ const defaultHost = opts.host || "127.0.0.1";
345
+ const defaultPort = opts.port || 3000;
346
+ const defaultPrefix = opts.prefix || "";
347
+ const defaultLogger = opts.logger !== false;
348
+ const content = `# Generated Fastify Application Environment Variables
349
+ #
350
+ # Copy this file to .env and customize as needed.
351
+ # Configuration precedence:
352
+ # 1. Environment variables (runtime overrides)
353
+ # 2. Catalog defaults (generation-time recorded values)
354
+ # 3. Hard defaults (see config file)
355
+
356
+ # WSDL source (required unless provided in catalog)
357
+ ${defaultWsdlSource ? `# Default from catalog: ${defaultWsdlSource}` : "# Required: specify the WSDL URL or local file path"}
358
+ ${defaultWsdlSource ? `#WSDL_SOURCE=${defaultWsdlSource}` : `WSDL_SOURCE=`}
359
+
360
+ # Server host (default: ${defaultHost})
361
+ HOST=${defaultHost}
362
+
363
+ # Server port (default: ${defaultPort})
364
+ PORT=${defaultPort}
365
+
366
+ # Route prefix (default: empty)
367
+ PREFIX=${defaultPrefix}
368
+
369
+ # Enable Fastify logger (default: ${defaultLogger})
370
+ LOGGER=${defaultLogger}
371
+
372
+ # Optional: SOAP security settings (configure based on your client requirements)
373
+ # SOAP_USERNAME=
374
+ # SOAP_PASSWORD=
375
+ # SOAP_ENDPOINT=
376
+ `;
377
+ fs.writeFileSync(path.join(appDir, ".env.example"), content, "utf-8");
378
+ }
379
+ /**
380
+ * Generates README.md file
381
+ *
382
+ * @param {string} appDir - App output directory
383
+ * @param {GenerateAppOptions} opts - App generation options
384
+ */
385
+ function generateReadme(appDir, opts) {
386
+ const imports = opts.imports || "js";
387
+ const ext = getExtension(imports);
388
+ const runCommand = ext === ".ts"
389
+ ? "npx tsx server.ts"
390
+ : "node server.js";
391
+ const content = `# Generated Fastify Application
392
+
393
+ This application was auto-generated by \`wsdl-tsc app\`.
394
+
395
+ ## Overview
396
+
397
+ This Fastify application provides a REST gateway to a SOAP service, automatically bridging between REST endpoints and SOAP operations.
398
+
399
+ ## Structure
400
+
401
+ - \`server${ext}\` - Main application entry point
402
+ - \`config${ext}\` - Configuration loader (environment-based)
403
+ - \`.env.example\` - Example environment configuration
404
+ - \`openapi.json\` - OpenAPI specification${opts.openapiMode === "copy" ? " (copied)" : " (referenced)"}
405
+
406
+ ## Prerequisites
407
+
408
+ - Node.js >= 20.0.0
409
+ - Dependencies installed (\`npm install\`)
410
+
411
+ ## Quick Start
412
+
413
+ 1. **Copy environment template**:
414
+ \`\`\`bash
415
+ cp .env.example .env
416
+ \`\`\`
417
+
418
+ 2. **Configure environment**:
419
+ Edit \`.env\` and set required variables (especially \`WSDL_SOURCE\` if not provided via catalog).
420
+
421
+ 3. **Run the server**:
422
+ \`\`\`bash
423
+ ${runCommand}
424
+ \`\`\`
425
+
426
+ ## Endpoints
427
+
428
+ ### Health Check
429
+ \`\`\`
430
+ GET /health
431
+ \`\`\`
432
+ Returns: \`{ ok: true }\`
433
+
434
+ ### OpenAPI Specification
435
+ \`\`\`
436
+ GET /openapi.json
437
+ \`\`\`
438
+ Returns: OpenAPI 3.1 specification document
439
+
440
+ ### Gateway Routes
441
+ All SOAP operations are exposed as REST endpoints. See \`openapi.json\` for complete API documentation.
442
+
443
+ ## Configuration
444
+
445
+ Configuration is loaded from environment variables with the following precedence:
446
+
447
+ 1. Environment variables (runtime overrides)
448
+ 2. Catalog defaults (from generation-time)
449
+ 3. Hard defaults (in config file)
450
+
451
+ ### Environment Variables
452
+
453
+ | Variable | Default | Description |
454
+ |----------|---------|-------------|
455
+ | \`WSDL_SOURCE\` | (from catalog) | WSDL URL or local file path (required) |
456
+ | \`HOST\` | ${opts.host || "127.0.0.1"} | Server bind address |
457
+ | \`PORT\` | ${opts.port || 3000} | Server listen port |
458
+ | \`PREFIX\` | ${opts.prefix || "(empty)"} | Route prefix |
459
+ | \`LOGGER\` | ${opts.logger !== false} | Enable Fastify logger |
460
+
461
+ ## Development
462
+
463
+ ### Running with watch mode
464
+ \`\`\`bash
465
+ npx tsx watch server${ext}
466
+ \`\`\`
467
+
468
+ ### Testing endpoints
469
+ \`\`\`bash
470
+ # Health check
471
+ curl http://localhost:3000/health
472
+
473
+ # OpenAPI spec
474
+ curl http://localhost:3000/openapi.json
475
+ \`\`\`
476
+
477
+ ## Troubleshooting
478
+
479
+ ### WSDL_SOURCE missing
480
+ If you see "WSDL_SOURCE environment variable is required", set it in your \`.env\` file or export it:
481
+ \`\`\`bash
482
+ export WSDL_SOURCE=path/to/service.wsdl
483
+ ${runCommand}
484
+ \`\`\`
485
+
486
+ ### Port already in use
487
+ Change the \`PORT\` in your \`.env\` file or:
488
+ \`\`\`bash
489
+ PORT=8080 ${runCommand}
490
+ \`\`\`
491
+
492
+ ## Notes
493
+
494
+ - This app uses the generated client from: \`${opts.clientDir}\`
495
+ - Gateway plugin from: \`${opts.gatewayDir}\`
496
+ - OpenAPI spec from: \`${opts.openapiFile}\`
497
+
498
+ ## Generated By
499
+
500
+ - Tool: [@techspokes/typescript-wsdl-client](https://github.com/TechSpokes/typescript-wsdl-client)
501
+ - Command: \`wsdl-tsc app\`
502
+ `;
503
+ fs.writeFileSync(path.join(appDir, "README.md"), content, "utf-8");
504
+ }
505
+ /**
506
+ * Generates a runnable Fastify application
507
+ *
508
+ * This function orchestrates the complete app generation process:
509
+ * 1. Validates all required inputs exist
510
+ * 2. Reads catalog.json for metadata
511
+ * 3. Creates app directory
512
+ * 4. Generates server.ts with Fastify setup
513
+ * 5. Generates config.ts with environment loading
514
+ * 6. Generates .env.example with configuration template
515
+ * 7. Generates README.md with usage instructions
516
+ * 8. Optionally copies OpenAPI spec into app directory
517
+ *
518
+ * @param {GenerateAppOptions} opts - App generation options
519
+ * @returns {Promise<void>}
520
+ * @throws {Error} If validation fails or required files are missing
521
+ */
522
+ export async function generateApp(opts) {
523
+ // Resolve all paths to absolute
524
+ const resolvedOpts = {
525
+ ...opts,
526
+ clientDir: path.resolve(opts.clientDir),
527
+ gatewayDir: path.resolve(opts.gatewayDir),
528
+ openapiFile: path.resolve(opts.openapiFile),
529
+ catalogFile: path.resolve(opts.catalogFile),
530
+ appDir: path.resolve(opts.appDir),
531
+ };
532
+ // Validate required files and directories
533
+ validateRequiredFiles(resolvedOpts);
534
+ // Read catalog for metadata
535
+ const catalog = readCatalog(resolvedOpts.catalogFile);
536
+ const clientClassName = deriveClientName(catalog);
537
+ const defaultWsdlSource = getCatalogWsdlSource(catalog);
538
+ // Create app directory
539
+ fs.mkdirSync(resolvedOpts.appDir, { recursive: true });
540
+ // Generate app files
541
+ generateServerFile(resolvedOpts.appDir, resolvedOpts, clientClassName);
542
+ generateConfigFile(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource);
543
+ generateEnvExample(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource);
544
+ generateReadme(resolvedOpts.appDir, resolvedOpts);
545
+ // Handle OpenAPI file
546
+ const openapiMode = resolvedOpts.openapiMode || "copy";
547
+ if (openapiMode === "copy") {
548
+ const destPath = path.join(resolvedOpts.appDir, "openapi.json");
549
+ fs.copyFileSync(resolvedOpts.openapiFile, destPath);
550
+ success(`Copied OpenAPI spec to ${destPath}`);
551
+ }
552
+ success(`Generated runnable Fastify app in ${resolvedOpts.appDir}`);
553
+ }