@common-stack/server-core 7.2.1-alpha.37 → 7.2.1-alpha.39

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,522 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate Zod schemas for ALL service interfaces (CommonJS version)
4
+ *
5
+ * This script can be run from:
6
+ * 1. Local project: node scripts/generateAllServiceSchemas.cjs
7
+ * 2. As npm script: Add to package.json scripts and run via npm/yarn
8
+ * 3. From node_modules: npx @common-stack/server-core generateAllServiceSchemas
9
+ *
10
+ * Configuration is loaded from cdecode-config.json in the project root.
11
+ * All paths in the config are relative to the project root.
12
+ *
13
+ * @example package.json script
14
+ * "scripts": {
15
+ * "generateSchemas": "node node_modules/@common-stack/server-core/lib/moleculer-generation/generateAllServiceSchemas.cjs"
16
+ * }
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const ts = require('typescript');
22
+
23
+ // Determine the project root - use current working directory (where the command is run)
24
+ // This works whether the script is in local scripts/ or node_modules/
25
+ const projectRoot = process.cwd();
26
+
27
+ // Load configuration from cdecode-config.json in the project root
28
+ const configPath = path.join(projectRoot, 'cdecode-config.json');
29
+ let serviceConfig = {};
30
+
31
+ if (fs.existsSync(configPath)) {
32
+ try {
33
+ const fullConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
34
+ serviceConfig = fullConfig.serviceSchemas || {};
35
+ console.log('✅ Loaded configuration from cdecode-config.json');
36
+ } catch (error) {
37
+ console.warn('⚠️ Could not parse cdecode-config.json, using defaults');
38
+ }
39
+ } else {
40
+ console.warn('⚠️ cdecode-config.json not found in project root, using defaults');
41
+ }
42
+
43
+ // Apply configuration with defaults (all paths relative to project root)
44
+ const SERVICES_DIR = path.join(projectRoot, serviceConfig.servicesDir || 'packages/common/src/services');
45
+ const OUTPUT_FILE = path.join(
46
+ projectRoot,
47
+ serviceConfig.outputFile || 'packages/common/src/generated/service-schemas.ts',
48
+ );
49
+
50
+ // Validate that SERVICES_DIR exists
51
+ if (!fs.existsSync(SERVICES_DIR)) {
52
+ console.error(`❌ Error: Services directory not found: ${SERVICES_DIR}`);
53
+ console.error(` Current working directory: ${projectRoot}`);
54
+ console.error(` Expected path (from config): ${serviceConfig.servicesDir || 'packages/common/src/services'}`);
55
+ console.error('\n Make sure:');
56
+ console.error(' 1. You are running this script from the project root');
57
+ console.error(' 2. The servicesDir path in cdecode-config.json is correct');
58
+ process.exit(1);
59
+ }
60
+
61
+ const SKIP_SERVICES = serviceConfig.skipServices || [
62
+ 'AuthBackendClient',
63
+ 'PubSub',
64
+ 'RedisCacheManager',
65
+ 'InngestClient',
66
+ 'JSONContributionRegistry',
67
+ 'PageStore',
68
+ 'PublisherEventService',
69
+ ];
70
+ const KNOWN_ENUMS = serviceConfig.knownEnums || [];
71
+ const CONST_ENUM_SCHEMAS = serviceConfig.constEnumSchemas || ['ConfigurationTarget', 'ConfigurationScope'];
72
+
73
+ /**
74
+ * Calculate relative path from outputFile to graphqlSchemasPath
75
+ */
76
+ function calculateRelativeImportPath() {
77
+ if (!serviceConfig.outputFile || !serviceConfig.graphqlSchemasPath) {
78
+ return './generated-zod-schemas';
79
+ }
80
+
81
+ const outputDir = path.dirname(path.join(projectRoot, serviceConfig.outputFile));
82
+ const graphqlSchemaPath = path.join(projectRoot, serviceConfig.graphqlSchemasPath);
83
+
84
+ // Calculate relative path
85
+ const relativePath = path.relative(outputDir, graphqlSchemaPath);
86
+
87
+ // Remove .ts extension and ensure it starts with ./
88
+ const withoutExt = relativePath.replace(/\.ts$/, '');
89
+ return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
90
+ }
91
+
92
+ const GRAPHQL_SCHEMAS_IMPORT_PATH = calculateRelativeImportPath();
93
+
94
+ /**
95
+ * Dynamically discover all available GraphQL Zod schemas from graphql-zod-schemas.ts
96
+ * This scans for exported schema functions and extracts their type names
97
+ */
98
+ function discoverGraphQLZodTypes() {
99
+ const graphqlSchemaFile = serviceConfig.graphqlSchemasPath
100
+ ? path.join(projectRoot, serviceConfig.graphqlSchemasPath)
101
+ : path.join(projectRoot, 'packages/common/src/generated/generated-zod-schemas.ts');
102
+
103
+ // Check if file exists
104
+ if (!fs.existsSync(graphqlSchemaFile)) {
105
+ console.warn('⚠️ graphql-zod-schemas.ts not found, using empty set for GraphQL types');
106
+ return new Set();
107
+ }
108
+
109
+ const content = fs.readFileSync(graphqlSchemaFile, 'utf8');
110
+ const graphqlTypes = new Set();
111
+
112
+ // Match all exported schema function names
113
+ // Pattern: export function SomeTypeInputSchema() or export function SomeTypeSchema()
114
+ const schemaFunctionPattern = /export function ([A-Z][a-zA-Z0-9]+)Schema\(\)/g;
115
+ let match;
116
+
117
+ while ((match = schemaFunctionPattern.exec(content)) !== null) {
118
+ const schemaName = match[1];
119
+
120
+ // Convert schema function name back to interface name
121
+ // Examples:
122
+ // - UserTokenInputSchema -> IUserTokenInput
123
+ // - UserAuth0UpdateFieldsSchema -> IUserAuth0UpdateFields
124
+ // - ConfigurationNodeInputSchema -> IConfigurationNodeInput
125
+
126
+ // Skip enum schemas (they don't have 'Input' suffix typically and are in CONST_ENUM_SCHEMAS)
127
+ if (CONST_ENUM_SCHEMAS.includes(schemaName)) {
128
+ continue;
129
+ }
130
+
131
+ // Add 'I' prefix to convert to interface name
132
+ const interfaceName = 'I' + schemaName;
133
+ graphqlTypes.add(interfaceName);
134
+ }
135
+
136
+ return graphqlTypes;
137
+ }
138
+
139
+ // Dynamically discover available GraphQL Zod types
140
+ const GRAPHQL_ZOD_TYPES = discoverGraphQLZodTypes();
141
+
142
+ function findServiceInterfaces() {
143
+ const files = fs.readdirSync(SERVICES_DIR).filter((f) => f.endsWith('.ts') && f !== 'index.ts');
144
+ const services = [];
145
+
146
+ files.forEach((file) => {
147
+ const content = fs.readFileSync(path.join(SERVICES_DIR, file), 'utf8');
148
+ const interfaceMatch = content.match(/export interface (I[A-Z][a-zA-Z0-9]*Service)/);
149
+
150
+ if (interfaceMatch) {
151
+ const serviceName = interfaceMatch[1];
152
+ if (!SKIP_SERVICES.includes(serviceName.replace(/^I/, ''))) {
153
+ services.push({
154
+ name: serviceName,
155
+ file: file,
156
+ path: path.join(SERVICES_DIR, file),
157
+ });
158
+ }
159
+ }
160
+ });
161
+
162
+ return services;
163
+ }
164
+
165
+ /**
166
+ * Parse a TypeScript source file and extract method signatures from an interface
167
+ */
168
+ function parseServiceInterface(filePath, interfaceName) {
169
+ const sourceCode = fs.readFileSync(filePath, 'utf8');
170
+ const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
171
+
172
+ const methods = [];
173
+ const usedEnums = new Set();
174
+
175
+ function visit(node) {
176
+ if (ts.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
177
+ node.members.forEach((member) => {
178
+ if (ts.isMethodSignature(member) && member.name) {
179
+ const methodName = member.name.getText(sourceFile);
180
+ const parameters = [];
181
+
182
+ if (member.parameters) {
183
+ member.parameters.forEach((param) => {
184
+ const paramName = param.name.getText(sourceFile);
185
+ const paramType = param.type ? param.type.getText(sourceFile) : 'any';
186
+ const isOptional = !!param.questionToken;
187
+
188
+ // Skip destructured parameters - treat the whole param as passthrough object
189
+ if (ts.isObjectBindingPattern(param.name)) {
190
+ // This is a destructured parameter like { user, accessToken }
191
+ // Use the type instead
192
+ parameters.push({
193
+ name: '_destructured',
194
+ type: paramType,
195
+ isOptional,
196
+ isDestructured: true,
197
+ });
198
+ return;
199
+ }
200
+
201
+ // Check if type is a known enum or const enum
202
+ KNOWN_ENUMS.forEach((enumName) => {
203
+ if (paramType.includes(enumName)) {
204
+ usedEnums.add(enumName);
205
+ }
206
+ });
207
+ CONST_ENUM_SCHEMAS.forEach((enumName) => {
208
+ if (paramType.includes(enumName)) {
209
+ usedEnums.add(enumName);
210
+ }
211
+ });
212
+
213
+ parameters.push({
214
+ name: paramName,
215
+ type: paramType,
216
+ isOptional,
217
+ });
218
+ });
219
+ }
220
+
221
+ methods.push({
222
+ name: methodName,
223
+ parameters,
224
+ });
225
+ }
226
+ });
227
+ }
228
+
229
+ ts.forEachChild(node, visit);
230
+ }
231
+
232
+ visit(sourceFile);
233
+ return { methods, usedEnums: Array.from(usedEnums) };
234
+ }
235
+
236
+ /**
237
+ * Generate Zod schema string for a parameter type
238
+ */
239
+ function generateZodType(paramType, isOptional, paramName = '') {
240
+ let zodType;
241
+
242
+ // Handle union types like "string | null | undefined"
243
+ if (paramType.includes('|')) {
244
+ const types = paramType.split('|').map(t => t.trim());
245
+ const hasNull = types.includes('null');
246
+ const hasUndefined = types.includes('undefined');
247
+ const nonNullableTypes = types.filter(t => t !== 'null' && t !== 'undefined');
248
+
249
+ if (nonNullableTypes.length === 1) {
250
+ // Single non-nullable type with null/undefined
251
+ zodType = generateZodType(nonNullableTypes[0], false, paramName);
252
+ if (hasNull || hasUndefined) {
253
+ return `${zodType}.optional().nullable()`;
254
+ }
255
+ return zodType;
256
+ } else if (nonNullableTypes.length > 1) {
257
+ // Multiple types - create union
258
+ const schemas = nonNullableTypes.map(t => generateZodType(t, false, paramName));
259
+ zodType = `z.union([${schemas.join(', ')}])`;
260
+ if (hasNull || hasUndefined) {
261
+ return `${zodType}.optional().nullable()`;
262
+ }
263
+ return zodType;
264
+ }
265
+ }
266
+
267
+ // Handle array types
268
+ if (paramType.includes('[]')) {
269
+ const innerType = paramType.replace('[]', '').trim();
270
+ const innerZodType = generateZodType(innerType, false, paramName);
271
+ zodType = `z.array(${innerZodType})`;
272
+ }
273
+ // Handle known enums
274
+ else if (KNOWN_ENUMS.some((e) => paramType.includes(e))) {
275
+ const enumName = KNOWN_ENUMS.find((e) => paramType.includes(e));
276
+ zodType = `z.nativeEnum(${enumName})`;
277
+ }
278
+ // Handle const enums (use schema functions from graphql-zod-schemas)
279
+ else if (CONST_ENUM_SCHEMAS.some((e) => paramType.includes(e))) {
280
+ const enumName = CONST_ENUM_SCHEMAS.find((e) => paramType.includes(e));
281
+ zodType = `${enumName}Schema()`;
282
+ }
283
+ // Handle primitive types
284
+ else if (paramType === 'string') {
285
+ zodType = 'z.string()';
286
+ } else if (paramType === 'number') {
287
+ zodType = 'z.number()';
288
+ } else if (paramType === 'boolean') {
289
+ zodType = 'z.boolean()';
290
+ } else if (paramType === 'any') {
291
+ zodType = 'z.any()';
292
+ }
293
+ // Handle Date
294
+ else if (paramType === 'Date') {
295
+ zodType = 'z.date()';
296
+ }
297
+ // Check if it's a GraphQL type with existing Zod schema
298
+ else if (GRAPHQL_ZOD_TYPES.has(paramType)) {
299
+ // Use the GraphQL-generated schema function
300
+ const schemaName = paramType.replace(/^I/, '') + 'Schema';
301
+ zodType = `${schemaName}()`;
302
+ }
303
+ // Handle Promise types (return type, not for validation)
304
+ else if (paramType.startsWith('Promise<')) {
305
+ zodType = 'z.any()';
306
+ }
307
+ // Use reusable schemas for common parameter names
308
+ else if (paramName === 'context' || paramName === 'userContext') {
309
+ zodType = 'ContextSchema';
310
+ } else if (paramName === 'resource') {
311
+ zodType = 'ResourceSchema';
312
+ } else if (paramName === 'options' || paramName === 'paginationOptions') {
313
+ zodType = 'OptionsSchema';
314
+ } else if (paramName === 'data' || paramName === 'updateData') {
315
+ zodType = 'DataSchema';
316
+ } else if (paramName === 'input') {
317
+ zodType = 'InputSchema';
318
+ } else if (paramName === 'filter' || paramName === 'criteria' || paramName === 'additionalFilters') {
319
+ zodType = 'FilterSchema';
320
+ } else if (paramName === 'where') {
321
+ zodType = 'WhereSchema';
322
+ } else if (paramName === 'request') {
323
+ zodType = 'RequestSchema';
324
+ }
325
+ // Default: treat as object
326
+ else {
327
+ zodType = 'PassthroughObjectSchema';
328
+ }
329
+
330
+ return isOptional ? `${zodType}.optional()` : zodType;
331
+ }
332
+
333
+ function generateServiceSchemas() {
334
+ const services = findServiceInterfaces();
335
+
336
+ console.log(`\n🔍 Found ${services.length} service interfaces\n`);
337
+
338
+ // Parse all services and collect used enums and GraphQL types
339
+ const allUsedEnums = new Set();
340
+ const allUsedGraphQLTypes = new Set();
341
+ const allUsedConstEnumSchemas = new Set(); // Track const enum schemas
342
+ const servicesWithMethods = [];
343
+
344
+ services.forEach((service) => {
345
+ const { methods, usedEnums } = parseServiceInterface(service.path, service.name);
346
+ usedEnums.forEach((e) => {
347
+ if (CONST_ENUM_SCHEMAS.includes(e)) {
348
+ allUsedConstEnumSchemas.add(e);
349
+ } else {
350
+ allUsedEnums.add(e);
351
+ }
352
+ });
353
+
354
+ // Track which GraphQL types are actually used
355
+ methods.forEach((method) => {
356
+ method.parameters.forEach((param) => {
357
+ if (GRAPHQL_ZOD_TYPES.has(param.type)) {
358
+ allUsedGraphQLTypes.add(param.type);
359
+ }
360
+ // Check in array types
361
+ if (param.type.includes('[]')) {
362
+ const innerType = param.type.replace('[]', '').trim();
363
+ if (GRAPHQL_ZOD_TYPES.has(innerType)) {
364
+ allUsedGraphQLTypes.add(innerType);
365
+ }
366
+ }
367
+ });
368
+ });
369
+
370
+ servicesWithMethods.push({
371
+ ...service,
372
+ methods,
373
+ });
374
+ });
375
+
376
+ // Start generating output
377
+ let output = `// AUTO-GENERATED FILE - DO NOT EDIT
378
+ // Generated from service interfaces in packages/common/src/services/
379
+ // Run: yarn generateGraphql to regenerate
380
+
381
+ import { z } from 'zod';
382
+ `;
383
+
384
+ // Import GraphQL Zod schemas (including const enum schemas)
385
+ const allGraphQLSchemas = new Set([...allUsedGraphQLTypes, ...allUsedConstEnumSchemas]);
386
+ if (allGraphQLSchemas.size > 0) {
387
+ output += `\n// Import GraphQL-generated Zod schemas for detailed validation\nimport {\n`;
388
+ allGraphQLSchemas.forEach((typeName) => {
389
+ const schemaName = typeName.replace(/^I/, '') + 'Schema';
390
+ output += ` ${schemaName},\n`;
391
+ });
392
+ output += `} from '${GRAPHQL_SCHEMAS_IMPORT_PATH}';\n`;
393
+ }
394
+
395
+ // Import used enums (only regular enums, not const enums)
396
+ if (allUsedEnums.size > 0) {
397
+ output += `\n// Import enums\nimport {\n`;
398
+ allUsedEnums.forEach((enumName) => {
399
+ output += ` ${enumName},\n`;
400
+ });
401
+ output += `} from 'common/server';\n`;
402
+ }
403
+
404
+ output += `
405
+ /**
406
+ * Centralized Zod schemas for all service interfaces
407
+ *
408
+ * Each service has its schemas exported as {ServiceName}Schemas
409
+ * Use with zodSchemasToMoleculer() for Moleculer parameter validation
410
+ *
411
+ * Example:
412
+ * \`\`\`typescript
413
+ * import { AccountServiceSchemas, zodSchemasToMoleculer } from 'common/server';
414
+ * const paramOverrides = zodSchemasToMoleculer(AccountServiceSchemas);
415
+ * \`\`\`
416
+ */
417
+
418
+ // Reusable schemas for common parameter types
419
+ const PassthroughObjectSchema = z.object({}).passthrough();
420
+ const ContextSchema = PassthroughObjectSchema;
421
+ const ResourceSchema = PassthroughObjectSchema;
422
+ const OptionsSchema = PassthroughObjectSchema;
423
+ const DataSchema = PassthroughObjectSchema;
424
+ const InputSchema = PassthroughObjectSchema;
425
+ const FilterSchema = PassthroughObjectSchema;
426
+ const WhereSchema = PassthroughObjectSchema;
427
+ const RequestSchema = PassthroughObjectSchema;
428
+
429
+ `;
430
+
431
+ // Generate schemas for each service
432
+ servicesWithMethods.forEach((service) => {
433
+ const schemaName = service.name.replace(/^I/, '').replace(/Service$/, 'Service');
434
+ const serviceName = service.name.replace(/^I/, ''); // e.g., "TagService"
435
+
436
+ output += `/**
437
+ * Zod schemas for ${service.name}
438
+ * Source: ${service.file}
439
+ */
440
+ export const ${schemaName}Schemas = {
441
+ /** Moleculer service topic/name - safe for minification */
442
+ topic: '${serviceName}' as const,
443
+ `;
444
+
445
+ if (service.methods.length === 0) {
446
+ output += ` // No methods found\n`;
447
+ } else {
448
+ // Group methods by name to handle overloads (keep last signature)
449
+ const methodMap = new Map();
450
+ service.methods.forEach((method) => {
451
+ methodMap.set(method.name, method);
452
+ });
453
+
454
+ // Generate schemas for unique methods
455
+ methodMap.forEach((method, methodName) => {
456
+ if (method.parameters.length === 0) {
457
+ output += ` ${methodName}: PassthroughObjectSchema,\n`;
458
+ } else {
459
+ // Check if this method has only destructured params
460
+ const hasOnlyDestructured = method.parameters.length === 1 && method.parameters[0].isDestructured;
461
+
462
+ if (hasOnlyDestructured) {
463
+ // For destructured params, use the type directly
464
+ const param = method.parameters[0];
465
+ const zodType = generateZodType(param.type, param.isOptional, param.name);
466
+ output += ` ${methodName}: ${zodType},\n`;
467
+ } else {
468
+ output += ` ${methodName}: z.object({\n`;
469
+ method.parameters.forEach((param) => {
470
+ if (!param.isDestructured) {
471
+ const zodType = generateZodType(param.type, param.isOptional, param.name);
472
+ output += ` ${param.name}: ${zodType},\n`;
473
+ }
474
+ });
475
+ output += ` }),\n`;
476
+ }
477
+ }
478
+ });
479
+ }
480
+
481
+ output += `};\n\n`;
482
+ });
483
+
484
+ // Generate aggregated export with explicit type annotation to avoid TS7056 error
485
+ output += `/**
486
+ * All service schemas aggregated
487
+ * Type annotation prevents TypeScript from inferring overly complex types
488
+ */
489
+ export const AllServiceSchemas: Record<string, Record<string, any>> = {\n`;
490
+
491
+ servicesWithMethods.forEach((service) => {
492
+ const schemaName = service.name.replace(/^I/, '').replace(/Service$/, 'Service');
493
+ output += ` ${service.name}: ${schemaName}Schemas,\n`;
494
+ });
495
+
496
+ output += `};\n`;
497
+
498
+ // Write the file
499
+ fs.writeFileSync(OUTPUT_FILE, output, 'utf8');
500
+
501
+ console.log(`✅ Generated schemas for ${services.length} services`);
502
+
503
+ // Count total methods
504
+ const totalMethods = servicesWithMethods.reduce((sum, s) => sum + s.methods.length, 0);
505
+ console.log(`📊 Total methods: ${totalMethods}`);
506
+ console.log(`📝 Output: ${path.relative(process.cwd(), OUTPUT_FILE)}\n`);
507
+
508
+ return services.length;
509
+ }
510
+
511
+ // Export for CommonJS
512
+ module.exports = { generateServiceSchemas };
513
+
514
+ // CLI execution
515
+ if (require.main === module) {
516
+ try {
517
+ generateServiceSchemas();
518
+ } catch (error) {
519
+ console.error('❌ Error generating service schemas:', error);
520
+ process.exit(1);
521
+ }
522
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@common-stack/server-core",
3
- "version": "7.2.1-alpha.37",
3
+ "version": "7.2.1-alpha.39",
4
4
  "description": "common core for higher packages to depend on",
5
5
  "license": "ISC",
6
6
  "author": "CDMBase LLC",
@@ -34,7 +34,7 @@
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "gitHead": "2075a0dd934b570fec84195170a672e32852540f",
37
+ "gitHead": "d6688e728527d5adb2f1947516463d504e94ef11",
38
38
  "typescript": {
39
39
  "definition": "lib/index.d.ts"
40
40
  }