@axinom/mosaic-cli 0.14.2-rc.9 → 0.15.0-rc.0

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.
Files changed (76) hide show
  1. package/package.json +6 -5
  2. package/src/cli/README.md +60 -0
  3. package/src/cli/index.ts +47 -0
  4. package/src/commands/apply-templates/apply-templates.spec.ts +623 -0
  5. package/src/commands/apply-templates/apply-templates.ts +494 -0
  6. package/src/commands/apply-templates/bitwarden-vault.ts +130 -0
  7. package/src/commands/apply-templates/index.ts +1 -0
  8. package/src/commands/create-extension-config/create-extension-config.ts +92 -0
  9. package/src/commands/create-extension-config/index.ts +23 -0
  10. package/src/commands/get-access-token/get-access-token-options.ts +9 -0
  11. package/src/commands/get-access-token/get-dev-access-token.ts +32 -0
  12. package/src/commands/get-access-token/index.ts +66 -0
  13. package/src/commands/graphql-diff.ts +143 -0
  14. package/src/commands/msg-codegen/codegen.ts +891 -0
  15. package/src/commands/msg-codegen/index.ts +48 -0
  16. package/src/commands/msg-codegen/lint.ts +84 -0
  17. package/src/commands/msg-codegen/message-codegen-options.ts +7 -0
  18. package/src/commands/msg-diff/asyncapi-override.ts +31 -0
  19. package/src/commands/msg-diff/git-checkout-tmp.ts +73 -0
  20. package/src/commands/msg-diff/index.ts +53 -0
  21. package/src/commands/msg-diff/message-diff-options.ts +7 -0
  22. package/src/commands/msg-diff/msg-diff.spec.ts +412 -0
  23. package/src/commands/msg-diff/msg-diff.ts +364 -0
  24. package/src/commands/msg-diff/test-resources/0/1-asyncapi.yml +38 -0
  25. package/src/commands/msg-diff/test-resources/0/2-asyncapi.yml +36 -0
  26. package/src/commands/msg-diff/test-resources/0/command.json +74 -0
  27. package/src/commands/msg-diff/test-resources/0/event.json +25 -0
  28. package/src/commands/msg-diff/test-resources/1/1-asyncapi.yml +25 -0
  29. package/src/commands/msg-diff/test-resources/1/moved-event.json +25 -0
  30. package/src/commands/msg-diff/test-resources/common.json +20 -0
  31. package/src/commands/pg-dump/README.md +21 -0
  32. package/src/commands/pg-dump/generate.ts +146 -0
  33. package/src/commands/pg-dump/index.ts +39 -0
  34. package/src/commands/pg-dump/pg-dump-options.ts +6 -0
  35. package/src/commands/publish-schema-to-db/README.md +130 -0
  36. package/src/commands/publish-schema-to-db/abstractions/base-smart-tags.ts +6 -0
  37. package/src/commands/publish-schema-to-db/abstractions/index.ts +5 -0
  38. package/src/commands/publish-schema-to-db/abstractions/pg-column.ts +31 -0
  39. package/src/commands/publish-schema-to-db/abstractions/pg-fk-column.ts +6 -0
  40. package/src/commands/publish-schema-to-db/abstractions/pg-table.ts +55 -0
  41. package/src/commands/publish-schema-to-db/abstractions/pg-type.ts +8 -0
  42. package/src/commands/publish-schema-to-db/content-entity-model.ts +93 -0
  43. package/src/commands/publish-schema-to-db/generate.ts +82 -0
  44. package/src/commands/publish-schema-to-db/index.ts +49 -0
  45. package/src/commands/publish-schema-to-db/jest.config.js +9 -0
  46. package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.spec.ts +42 -0
  47. package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.ts +41 -0
  48. package/src/commands/publish-schema-to-db/pg-models/columns/index.ts +4 -0
  49. package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.spec.ts +47 -0
  50. package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.ts +34 -0
  51. package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.spec.ts +65 -0
  52. package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.ts +62 -0
  53. package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.spec.ts +24 -0
  54. package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.ts +34 -0
  55. package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.spec.ts +182 -0
  56. package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.ts +166 -0
  57. package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.spec.ts +19 -0
  58. package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.ts +237 -0
  59. package/src/commands/publish-schema-to-db/pg-models/pgl-utils.spec.ts +19 -0
  60. package/src/commands/publish-schema-to-db/pg-models/pgl-utils.ts +115 -0
  61. package/src/commands/publish-schema-to-db/pg-models/tables/content-entity-table.ts +104 -0
  62. package/src/commands/publish-schema-to-db/pg-models/tables/index.ts +3 -0
  63. package/src/commands/publish-schema-to-db/pg-models/tables/object-property-table.ts +113 -0
  64. package/src/commands/publish-schema-to-db/pg-models/tables/relations-table.ts +115 -0
  65. package/src/commands/publish-schema-to-db/postprocessors/collection-postprocessor.ts +33 -0
  66. package/src/commands/publish-schema-to-db/postprocessors/content-entity-model-postprocessor.ts +13 -0
  67. package/src/commands/publish-schema-to-db/postprocessors/episode-postprocessor.ts +37 -0
  68. package/src/commands/publish-schema-to-db/postprocessors/index.ts +6 -0
  69. package/src/commands/publish-schema-to-db/postprocessors/movie-postprocessor.ts +30 -0
  70. package/src/commands/publish-schema-to-db/postprocessors/postprocessing-utils.ts +21 -0
  71. package/src/commands/publish-schema-to-db/postprocessors/season-postprocessor.ts +37 -0
  72. package/src/commands/publish-schema-to-db/postprocessors/tvshow-postprocessor.ts +30 -0
  73. package/src/commands/publish-schema-to-db/publish-schema-to-db-options.ts +15 -0
  74. package/src/commands/publish-schema-to-db/types/sql-formatter.d.ts +10 -0
  75. package/src/exports.ts +2 -0
  76. package/src/index.ts +1 -0
@@ -0,0 +1,891 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import {
4
+ CommonNamingConvention,
5
+ FormatHelpers,
6
+ OutputModel,
7
+ PropertyType,
8
+ TypeScriptGenerator,
9
+ } from '@asyncapi/modelina';
10
+ import { AsyncAPIDocument, Message, parse } from '@asyncapi/parser';
11
+ import endent from 'endent';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { MessageCodegenOptions } from './message-codegen-options';
15
+
16
+ /**
17
+ * Processes AsyncAPI document and generates Typescript classes from it.
18
+ * Generated output:
19
+ * - message payloads will be transformed into Typescript types and bundled JSON schemas
20
+ * - channels information will be used to generate MessagingSettings, that can be used to configure RabbitMQ connection.
21
+ * @param inputDir - Path to input directory - AsyncAPI document root.
22
+ * @param filePattern - Regular expression that matches suitable input files.
23
+ * @param outputDir - Path to output directory - output root.
24
+ */
25
+ export class Codegen {
26
+ private readonly schemaRoot: string;
27
+ private readonly filePattern: RegExp;
28
+ private readonly outputRoot: string;
29
+ private readonly typesOutputRoot: string;
30
+ private readonly schemasOutputRoot: string;
31
+ private readonly messagingSettingsOutputRoot: string;
32
+
33
+ constructor(options: MessageCodegenOptions) {
34
+ this.schemaRoot = path.resolve(options.inputDir);
35
+ try {
36
+ this.filePattern = new RegExp(options.filePattern);
37
+ } catch (error) {
38
+ console.error(
39
+ `${options.filePattern} is not a valid regular expression.`,
40
+ );
41
+ process.exit(1);
42
+ }
43
+
44
+ this.outputRoot = path.resolve(options.outputDir);
45
+ this.typesOutputRoot = path.join(this.outputRoot, 'types');
46
+ this.schemasOutputRoot = path.join(this.outputRoot, 'schemas');
47
+ this.messagingSettingsOutputRoot = path.join(this.outputRoot, 'config');
48
+ }
49
+
50
+ public async run(): Promise<void> {
51
+ console.log('Running message codegen.');
52
+ console.log(`* schema root: ${this.schemaRoot}`);
53
+ console.log(`* output root: ${this.outputRoot}`);
54
+
55
+ await this.walk(this.schemaRoot);
56
+ await this.barrelExportTs(
57
+ [],
58
+ [
59
+ this.typesOutputRoot,
60
+ this.schemasOutputRoot,
61
+ this.messagingSettingsOutputRoot,
62
+ ],
63
+ path.join(this.outputRoot, 'index.ts'),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Recursively walks a schemas directory and generates TS + bundled JSON schema files.
69
+ * @param dir - Directory to walk.
70
+ */
71
+ async walk(dir: string): Promise<void> {
72
+ const items = await fs.promises.readdir(dir);
73
+
74
+ const fullPathItems = items.map((i) => path.join(dir, i));
75
+ const dirs = fullPathItems.filter((i) => fs.statSync(i).isDirectory());
76
+ const files = fullPathItems.filter(
77
+ (i) => fs.statSync(i).isFile() && this.filePattern.test(i),
78
+ );
79
+
80
+ if ([...files, ...dirs].length === 0) {
81
+ return;
82
+ }
83
+
84
+ for (const asyncApiFile of files) {
85
+ console.log(`Processing ${asyncApiFile}.`);
86
+ await this.processAsyncAPIDocument(asyncApiFile);
87
+ }
88
+
89
+ for (const directory of dirs) {
90
+ await this.walk(directory);
91
+ }
92
+
93
+ if (files.length === 0) {
94
+ //Calculate correct TS output related directories for barrel exports.
95
+ const tsDirs = dirs.map((d) =>
96
+ path.join(this.typesOutputRoot, path.relative(this.schemaRoot, d)),
97
+ );
98
+ const tsIndexOutPath = path.join(
99
+ this.typesOutputRoot,
100
+ getRelativeDir(this.schemaRoot, dir),
101
+ 'index.ts',
102
+ );
103
+ await this.barrelExportTs([], tsDirs, tsIndexOutPath);
104
+
105
+ // Calculate correct JSON schema output related directories for barrel exports.
106
+ const schemaDirs = dirs.map((d) =>
107
+ path.join(this.schemasOutputRoot, path.relative(this.schemaRoot, d)),
108
+ );
109
+ const schemaIndexOutPath = path.join(
110
+ this.schemasOutputRoot,
111
+ getRelativeDir(this.schemaRoot, dir),
112
+ 'index.ts',
113
+ );
114
+ await this.barrelExportSchema([], schemaDirs, schemaIndexOutPath);
115
+
116
+ // Calculate correct Message Settings output related directories for barrel exports.
117
+ const settingsDirs = dirs.map((d) =>
118
+ path.join(
119
+ this.messagingSettingsOutputRoot,
120
+ path.relative(this.schemaRoot, d),
121
+ ),
122
+ );
123
+ const messagingIndexOutPath = path.join(
124
+ this.messagingSettingsOutputRoot,
125
+ getRelativeDir(this.schemaRoot, dir),
126
+ 'index.ts',
127
+ );
128
+ await this.barrelExportSettings([], settingsDirs, messagingIndexOutPath);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Builds a path for the generated TS file from an input JSON schema path, retains directory structure.
134
+ * @param outputPath - output directory.
135
+ * @param typeName - Name of the type.
136
+ */
137
+ buildTsOutPath(outputPath: string, typeName: string): string {
138
+ return path.join(outputPath, `${FormatHelpers.toParamCase(typeName)}.ts`);
139
+ }
140
+
141
+ /**
142
+ * Builds a path for the bundled JSON schema file from an input JSON schema path, retains directory structure.
143
+ * @param outputPath - output directory.
144
+ * @param typeName - Name of the type.
145
+ */
146
+ buildSchemaOutPath(outputPath: string, typeName: string): string {
147
+ return path.join(outputPath, `${FormatHelpers.toParamCase(typeName)}.json`);
148
+ }
149
+
150
+ /**
151
+ * Generates TS interfaces and JSON schemas from AsyncAPI schema.
152
+ * @param asyncApiFile - path to AsyncAPI document
153
+ */
154
+ async processAsyncAPIDocument(asyncApiFile: string): Promise<void> {
155
+ const schemaContent = fs.readFileSync(asyncApiFile, 'utf8');
156
+
157
+ const asyncApiDocument = await parse(schemaContent, { path: asyncApiFile });
158
+
159
+ //export Typescript models
160
+ const tsModelsOutputPath = path.join(
161
+ this.typesOutputRoot,
162
+ getRelativeDir(this.schemaRoot, asyncApiFile),
163
+ );
164
+ await this.exportTsModels(asyncApiDocument, tsModelsOutputPath);
165
+
166
+ //export JSON Schemas
167
+ const schemasOutputPath = path.join(
168
+ this.schemasOutputRoot,
169
+ getRelativeDir(this.schemaRoot, asyncApiFile),
170
+ );
171
+ await this.exportSchemas(asyncApiDocument, schemasOutputPath);
172
+
173
+ //export message settings
174
+ const messagingSettingsOutputPath = path.join(
175
+ this.messagingSettingsOutputRoot,
176
+ getRelativeDir(this.schemaRoot, asyncApiFile),
177
+ );
178
+ await this.exportSettings(asyncApiDocument, messagingSettingsOutputPath);
179
+ }
180
+
181
+ /**
182
+ * Export all TS models from AsyncAPI document
183
+ * @param asyncAPIDocument - AsyncAPI Document object
184
+ * @param outputPath - output path for generated files
185
+ */
186
+ async exportTsModels(
187
+ asyncAPIDocument: AsyncAPIDocument,
188
+ outputPath: string,
189
+ ): Promise<void> {
190
+ const generator = new TypeScriptGenerator({
191
+ modelType: 'interface',
192
+ namingConvention: CustomNamingConvention,
193
+ enumType: 'union',
194
+ presets: [
195
+ ADDITIONAL_PROPERTIES_PRESET,
196
+ NULLABLE_PROPERTY_TO_UNION_PRESET,
197
+ EXPORT_UNION_TYPE_PRESET,
198
+ EXPORT_INTERFACE_PRESET,
199
+ DESCRIPTION_PRESET,
200
+ IMPORTS_PRESET,
201
+ ],
202
+ });
203
+
204
+ let outputModels = await await generator.generate(asyncAPIDocument as any);
205
+ for (const key in asyncAPIDocument.components().schemas()) {
206
+ const referencedModels = await generator.generate(
207
+ asyncAPIDocument.components().schemas()[key].json(),
208
+ );
209
+ outputModels = outputModels.map(
210
+ (om) =>
211
+ referencedModels.find((rm) => om.modelName === rm.modelName) || om,
212
+ );
213
+ }
214
+
215
+ const groupedModels: { [key: string]: OutputModel[] } = {};
216
+ outputModels.map((outModel) => {
217
+ const modelPathPrefix = getModelPathPrefix(outModel.modelName);
218
+ if (groupedModels[modelPathPrefix] === undefined) {
219
+ groupedModels[modelPathPrefix] = [];
220
+ }
221
+ groupedModels[modelPathPrefix].push(outModel);
222
+ });
223
+
224
+ const tsDirs: string[] = [];
225
+ for (const pathPrefix in groupedModels) {
226
+ const models = groupedModels[pathPrefix];
227
+ const groupOutPutPath = path.join(outputPath, pathPrefix);
228
+ if (pathPrefix) {
229
+ tsDirs.push(groupOutPutPath);
230
+ }
231
+ await fs.promises.mkdir(groupOutPutPath, {
232
+ recursive: true,
233
+ });
234
+ const tsFiles: string[] = [];
235
+ for (const outputModel of models) {
236
+ const outputFilePath = this.buildTsOutPath(
237
+ groupOutPutPath,
238
+ outputModel.modelName,
239
+ );
240
+ await fs.promises.writeFile(outputFilePath, outputModel.result);
241
+ tsFiles.push(outputFilePath);
242
+ }
243
+ //generate barrel export for all model in AsyncAPI document
244
+ const tsIndexOutPath = path.join(groupOutPutPath, 'index.ts');
245
+ await this.barrelExportTs(tsFiles, [], tsIndexOutPath);
246
+ }
247
+ if (tsDirs.length > 0) {
248
+ const tsIndexOutPath = path.join(outputPath, 'index.ts');
249
+ await this.barrelExportTs([], tsDirs, tsIndexOutPath);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Export all JSON Schemas from AsyncAPI document
255
+ * @param asyncAPIDocument - AsyncAPI Document object
256
+ * @param outputPath - output path for generated files
257
+ */
258
+ async exportSchemas(
259
+ asyncAPIDocument: AsyncAPIDocument,
260
+ outputPath: string,
261
+ ): Promise<void> {
262
+ const groupedSchemas: { [key: string]: Message[] } = {};
263
+
264
+ for (const [, message] of asyncAPIDocument.allMessages()) {
265
+ const schemaPathPrefix = getModelPathPrefix(getMessageTitle(message));
266
+ if (groupedSchemas[schemaPathPrefix] === undefined) {
267
+ groupedSchemas[schemaPathPrefix] = [];
268
+ }
269
+ groupedSchemas[schemaPathPrefix].push(message);
270
+ }
271
+ const schemaDirs: string[] = [];
272
+ for (const pathPrefix in groupedSchemas) {
273
+ const messages = groupedSchemas[pathPrefix];
274
+ const schemaFiles: string[] = [];
275
+ const groupOutPutPath = path.join(outputPath, pathPrefix);
276
+ if (pathPrefix) {
277
+ schemaDirs.push(groupOutPutPath);
278
+ }
279
+ await fs.promises.mkdir(groupOutPutPath, {
280
+ recursive: true,
281
+ });
282
+ for (const msg of messages) {
283
+ const outputSchemaPath = this.buildSchemaOutPath(
284
+ groupOutPutPath,
285
+ getMessageTitle(msg),
286
+ );
287
+ await this.bundleSchema(msg.originalPayload(), outputSchemaPath);
288
+ schemaFiles.push(outputSchemaPath);
289
+ }
290
+
291
+ //generate barrel export for all model in AsyncAPI document
292
+ const schemasIndexOutPath = path.join(groupOutPutPath, 'index.ts');
293
+ await this.barrelExportSchema(schemaFiles, [], schemasIndexOutPath);
294
+ }
295
+
296
+ const schemaIndexOutPath = path.join(outputPath, 'index.ts');
297
+ await this.barrelExportSchema([], schemaDirs, schemaIndexOutPath);
298
+ }
299
+
300
+ /**
301
+ * Export all AsyncAPI document channels to Messaging Settings
302
+ * @param asyncAPIDocument - AsyncAPI Document object
303
+ * @param outputPath - output path for generated files
304
+ */
305
+ async exportSettings(
306
+ asyncAPIDocument: AsyncAPIDocument,
307
+ outputPath: string,
308
+ ): Promise<void> {
309
+ const serviceTitle = getServiceTitle(asyncAPIDocument);
310
+ const serviceId = getServiceId(asyncAPIDocument);
311
+ const channelsData: ChannelData[] = [];
312
+
313
+ for (const routingKey of asyncAPIDocument.channelNames()) {
314
+ const channel = asyncAPIDocument.channel(routingKey);
315
+ if (channel.hasBinding('amqp')) {
316
+ const queueName = channel.binding('amqp')['queue']['name'];
317
+
318
+ const payloadName = channel.hasSubscribe()
319
+ ? channel.subscribe().message().payload().title()
320
+ : channel.hasPublish()
321
+ ? channel.publish().message().payload().title()
322
+ : 'undefined';
323
+
324
+ channelsData.push({
325
+ routingKey: routingKey,
326
+ queueName: queueName,
327
+ payloadName: payloadName,
328
+ acceptedAction: channel.hasSubscribe() ? 'subscribe' : `publish`,
329
+ isMultiTenant: routingKey.includes('*.*'),
330
+ });
331
+ }
332
+ }
333
+ await this.generateMessagingSettings(
334
+ serviceId,
335
+ serviceTitle,
336
+ channelsData,
337
+ outputPath,
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Generate Messaging Settings based on Channels data.
343
+ * @param serviceId - service identifier
344
+ * @param serviceTitle - service title
345
+ * @param channelsData - AsyncAPI channels information
346
+ * @param outputPath - output path for generated files
347
+ */
348
+ async generateMessagingSettings(
349
+ serviceId: string,
350
+ serviceTitle: string,
351
+ channelsData: ChannelData[],
352
+ outputPath: string,
353
+ ): Promise<void> {
354
+ const messagingSettingFiles: string[] = [];
355
+
356
+ messagingSettingFiles.push(
357
+ await this.createMessagingSettingFile(
358
+ true,
359
+ serviceId,
360
+ serviceTitle,
361
+ channelsData.filter((data) => data.isMultiTenant === true),
362
+ outputPath,
363
+ ),
364
+ );
365
+ messagingSettingFiles.push(
366
+ await this.createMessagingSettingFile(
367
+ false,
368
+ serviceId,
369
+ serviceTitle,
370
+ channelsData.filter((data) => data.isMultiTenant === false),
371
+ outputPath,
372
+ ),
373
+ );
374
+
375
+ //barrel export for messaging settings
376
+ const schemaIndexOutPath = path.join(outputPath, 'index.ts');
377
+ await this.barrelExportSettings(
378
+ messagingSettingFiles.filter((f) => f),
379
+ [],
380
+ schemaIndexOutPath,
381
+ );
382
+ }
383
+
384
+ /**
385
+ * Create new Messaging Settings file.
386
+ * @param isMultiTenant - is file with multi tenant settings, or not
387
+ * @param serviceId - service Identifier
388
+ * @param serviceTitle - service Title
389
+ * @param channelsData - AsyncAPI channels information
390
+ * @param outputPath - output path for generated files
391
+ */
392
+ async createMessagingSettingFile(
393
+ isMultiTenant: boolean,
394
+ serviceId: string,
395
+ serviceTitle: string,
396
+ channelsData: ChannelData[],
397
+ outputPath: string,
398
+ ): Promise<string> {
399
+ if (channelsData.length === 0) {
400
+ return ``;
401
+ }
402
+ const baseClass = isMultiTenant
403
+ ? `MultiTenantMessagingSettings`
404
+ : `MessagingSettings`;
405
+ const className = `${FormatHelpers.toPascalCase(serviceTitle)}${baseClass}`;
406
+ const action = isMultiTenant ? `extends` : `implements`;
407
+ const constructor = endent`private constructor(
408
+ public readonly messageType: string,
409
+ public readonly queue: string,
410
+ public readonly routingKey: string,
411
+ ) {
412
+ ${
413
+ isMultiTenant
414
+ ? `super('${serviceId}', messageType, queue, routingKey);`
415
+ : ``
416
+ }
417
+ }`;
418
+
419
+ const properties = channelsData.map((data) =>
420
+ this.createMessagingSettingProperty(data, className),
421
+ );
422
+ const content = endent`import { ${baseClass} } from '@axinom/mosaic-message-bus-abstractions';
423
+
424
+ export class ${className} ${action} ${baseClass} {
425
+ ${properties.join(`\n`)}
426
+ ${isMultiTenant ? `` : `\npublic readonly serviceId = '${serviceId}';\n`}
427
+ ${constructor}
428
+
429
+ public toString = (): string => {
430
+ return this.messageType;
431
+ };
432
+ }`;
433
+
434
+ const filePath = path.join(
435
+ outputPath,
436
+ `${FormatHelpers.toParamCase(className)}.ts`,
437
+ );
438
+ await fs.promises.mkdir(path.dirname(filePath), {
439
+ recursive: true,
440
+ });
441
+ await fs.promises.writeFile(filePath, content);
442
+
443
+ return filePath;
444
+ }
445
+ /**
446
+ * Create Messaging Settings property from Channel Data.
447
+ * @param channelData - AsyncAPI channel information
448
+ * @param settingsClassName - class name of messaging setting
449
+ */
450
+ createMessagingSettingProperty(
451
+ channelData: ChannelData,
452
+ settingsClassName: string,
453
+ ): string {
454
+ const messageType = FormatHelpers.toPascalCase(
455
+ channelData.payloadName.replace('command', '').replace('event', ''),
456
+ );
457
+ return endent`
458
+ public static ${messageType} = new ${settingsClassName}(
459
+ '${messageType}',
460
+ '${channelData.queueName}',
461
+ '${channelData.routingKey}'
462
+ );`;
463
+ }
464
+
465
+ /**
466
+ * Bundles a JSON schema into a self-contained file by including all external refs.
467
+ * @param jsonSchema - Schema object.
468
+ * @param outPath - Output JSON schema path.
469
+ */
470
+ async bundleSchema(jsonSchema: unknown, outPath: string): Promise<void> {
471
+ await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
472
+ await fs.promises.writeFile(outPath, JSON.stringify(jsonSchema, null, 2));
473
+ }
474
+
475
+ // TODO: Consider doing named exports based on message groups to not just put all messages in one namespace.
476
+ // TODO: Maybe some templating engine would be better than just building strings.
477
+ /**
478
+ * Generates a barrel index.ts for all modules inside `outPath`.
479
+ * In addition it generates a schemas enum for the included modules to make it easier to map them to bundled JSON schemas.
480
+ * @param files - Files to roll up.
481
+ * @param dirs - Directories to roll up.
482
+ * @param outPath - Path where to write `index.ts`.
483
+ */
484
+ async barrelExportTs(
485
+ files: string[],
486
+ dirs: string[],
487
+ outPath: string,
488
+ ): Promise<void> {
489
+ console.log(`Rolling up TS exports to ${outPath}.`);
490
+ const items = [
491
+ ...files.map((f) => path.basename(f, '.ts')),
492
+ ...dirs.filter((d) => fs.existsSync(d)).map((d) => path.basename(d)),
493
+ ];
494
+
495
+ if (items.length < 1) {
496
+ return;
497
+ }
498
+
499
+ const exports = `${items
500
+ .sort()
501
+ .map((p) => `export * from './${p}';`)
502
+ .join('\n')}`;
503
+ let schemaEnum = '';
504
+ let typeNamesEnum = '';
505
+
506
+ // TODO: Break this up into smaller pieces.
507
+ // TODO: Consider adding docstring to generated enums.
508
+ if (files.length > 0) {
509
+ const sortedFiles = files.sort();
510
+ // TODO: Message envelope is handled separately, we could remove this entirely.
511
+ // If message-envelope requires some special handling.
512
+ if (files.length === 1 && files[0].endsWith('message-envelope.ts')) {
513
+ const file = files[0];
514
+ schemaEnum = endent`
515
+ export enum MessageEnvelopeSchema {
516
+ MessageEnvelope = '${toPosixPath(this.tsPathToSchemaPath(file))}'
517
+ }
518
+ `;
519
+ } else {
520
+ let enumNameBase = FormatHelpers.toPascalCase(
521
+ getRelativeDir(this.typesOutputRoot, path.dirname(outPath)),
522
+ );
523
+ if (!enumNameBase.includes('Types')) {
524
+ enumNameBase = enumNameBase.replace('Payloads', ''); // Remove the Payloads prefix to reduce noise
525
+ schemaEnum = endent`
526
+ export enum ${enumNameBase}Schemas {
527
+ ${sortedFiles
528
+ .map(
529
+ (f) =>
530
+ `${FormatHelpers.toPascalCase(
531
+ path.basename(f, '.ts'),
532
+ )} = '${toPosixPath(this.tsPathToSchemaPath(f))}'`,
533
+ )
534
+ .join(',\n')}
535
+ }`;
536
+ typeNamesEnum = endent`
537
+ export enum ${enumNameBase}Types {
538
+ ${sortedFiles
539
+ .sort()
540
+ .map(
541
+ (f) =>
542
+ `${FormatHelpers.toPascalCase(
543
+ path.basename(f, '.ts'),
544
+ )} = '${FormatHelpers.toPascalCase(path.basename(f, '.ts'))}'`,
545
+ )
546
+ .join(',\n')}
547
+ }`;
548
+ }
549
+ }
550
+ }
551
+
552
+ const contents = endent`
553
+ ${exports}
554
+
555
+ ${schemaEnum}
556
+
557
+ ${typeNamesEnum}
558
+ `;
559
+
560
+ await fs.promises.writeFile(outPath, contents);
561
+ }
562
+
563
+ /**
564
+ * Generates a barrel index.ts for all Messaging Settings inside `outPath`.
565
+ * @param files - Files to roll up.
566
+ * @param dirs - Directories to roll up.
567
+ * @param outPath - Path where to write `index.ts`.
568
+ */
569
+ async barrelExportSettings(
570
+ files: string[],
571
+ dirs: string[],
572
+ outPath: string,
573
+ ): Promise<void> {
574
+ console.log(`Rolling up Messaging Settings exports to ${outPath}.`);
575
+ const items = [
576
+ ...files.map((f) => path.basename(f, '.ts')),
577
+ ...dirs.filter((d) => fs.existsSync(d)).map((d) => path.basename(d)),
578
+ ];
579
+
580
+ const dirExports = dirs
581
+ .sort()
582
+ .filter((d) => fs.existsSync(d))
583
+ .map((d) => path.basename(d));
584
+
585
+ if (items.length < 1) {
586
+ return;
587
+ }
588
+
589
+ let contents = '';
590
+
591
+ if (files.length > 0) {
592
+ const baseNames = files.map((f) => path.basename(f, '.ts')).sort();
593
+ const messagingImports = baseNames.map((n) => `export * from './${n}';`);
594
+ contents = endent`
595
+ ${messagingImports.join('\n')}
596
+ `;
597
+ }
598
+
599
+ contents += endent`\n
600
+ ${dirExports.map((d) => `export * from './${d}';`).join('\n')}`;
601
+
602
+ await fs.promises.writeFile(outPath, contents);
603
+ }
604
+ /**
605
+ * Generates a barrel index.ts for all JSON schemas inside `outPath`.
606
+ * @param files - Files to roll up.
607
+ * @param dirs - Directories to roll up.
608
+ * @param outPath - Path where to write `index.ts`.
609
+ */
610
+ async barrelExportSchema(
611
+ files: string[],
612
+ dirs: string[],
613
+ outPath: string,
614
+ ): Promise<void> {
615
+ console.log(`Rolling up JSON exports to ${outPath}.`);
616
+ const items = [
617
+ ...files.map((f) => path.basename(f, '.json')),
618
+ ...dirs.filter((d) => fs.existsSync(d)).map((d) => path.basename(d)),
619
+ ];
620
+
621
+ const dirExports = dirs
622
+ .sort()
623
+ .filter((d) => fs.existsSync(d))
624
+ .map((d) => path.basename(d));
625
+
626
+ if (items.length < 1) {
627
+ return;
628
+ }
629
+
630
+ let contents = '';
631
+
632
+ if (files.length > 0) {
633
+ const baseNames = files.map((f) => path.basename(f, '.json')).sort();
634
+ const schemaImports = baseNames.map(
635
+ (n) =>
636
+ `import * as ${FormatHelpers.toPascalCase(n)} from './${n}.json';`,
637
+ );
638
+ const schemaExports = baseNames.map(
639
+ (n) =>
640
+ `export const ${FormatHelpers.toPascalCase(
641
+ n,
642
+ )}Schema = ${FormatHelpers.toPascalCase(n)};`,
643
+ );
644
+ contents = endent`
645
+ ${schemaImports.join('\n')}
646
+
647
+ ${schemaExports.join('\n')}
648
+ `;
649
+ }
650
+
651
+ contents += endent`\n
652
+ ${dirExports.map((d) => `export * from './${d}';`).join('\n')}`;
653
+
654
+ await fs.promises.writeFile(outPath, contents);
655
+ }
656
+
657
+ /**
658
+ * Converts a generated TS path to a corresponding JSON schema path.
659
+ * @param tsFile - Path to a generated TS file.
660
+ */
661
+ tsPathToSchemaPath(tsFile: string): string {
662
+ return path.join(
663
+ getRelativeDir(this.typesOutputRoot, tsFile),
664
+ `${path.basename(tsFile, '.ts')}.json`,
665
+ );
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Metadata of channels for settings generation.
671
+ */
672
+ interface ChannelData {
673
+ /** Channel routing key */
674
+ routingKey: string;
675
+
676
+ /** Channel queue */
677
+ queueName: string;
678
+
679
+ /** Channel model */
680
+ payloadName: string;
681
+
682
+ /** Accepted Action*/
683
+ acceptedAction: 'subscribe' | 'publish';
684
+
685
+ /** Channel multi tenancy */
686
+ isMultiTenant: boolean;
687
+ }
688
+
689
+ function getRelativeDir(from: string, to: string): string {
690
+ return fs.statSync(to).isFile()
691
+ ? path.dirname(path.relative(from, to))
692
+ : path.relative(from, to);
693
+ }
694
+
695
+ function getMessageTitle(message: Message): string {
696
+ return (
697
+ message.payload().title() ??
698
+ message.extension('x-parser-message-name') ??
699
+ message.payload().extension('x-parser-schema-id')
700
+ );
701
+ }
702
+
703
+ /**
704
+ * Retrieve Service Title from AsyncAPI document.
705
+ * As Title returned value set in info.title property of document.
706
+ * @param asyncAPIDocument - AsyncAPI document.
707
+ */
708
+ function getServiceTitle(asyncAPIDocument: AsyncAPIDocument): string {
709
+ return asyncAPIDocument.info().title();
710
+ }
711
+
712
+ /**
713
+ * Retrieve Service Id from AsyncAPI document.
714
+ * As Id returned value set in extension property `x-service-id`.
715
+ * If extension property is not set - returned Service Title in lower-kebab-case.
716
+ * @param asyncAPIDocument - AsyncAPI document.
717
+ */
718
+ function getServiceId(asyncAPIDocument: AsyncAPIDocument): string {
719
+ return (
720
+ (asyncAPIDocument.extension('x-service-id') as string) ??
721
+ FormatHelpers.toParamCase(getServiceTitle(asyncAPIDocument))
722
+ );
723
+ }
724
+
725
+ /**
726
+ * Converts a path into a POSIX path by replacing current path separator with `/`.
727
+ * @param p - Path to convert.
728
+ */
729
+ function toPosixPath(p: string): string {
730
+ return path.posix.join(...p.split(path.sep));
731
+ }
732
+
733
+ /**
734
+ * Generating the path prefix for model.
735
+ * model name ends with "command" - prefix "commands"
736
+ * model name ends with "event" - prefix "events"
737
+ * default prefix "types"
738
+ * @param modelName - name of the model, defined in json schema, or AsyncAPI document
739
+ */
740
+ function getModelPathPrefix(modelName: string): string {
741
+ return modelName
742
+ ? modelName.toLowerCase().endsWith('command')
743
+ ? 'commands'
744
+ : modelName.toLowerCase().endsWith('event')
745
+ ? 'events'
746
+ : 'types'
747
+ : 'types';
748
+ }
749
+
750
+ /**
751
+ * Adding 'export' for interfaces
752
+ */
753
+ const EXPORT_INTERFACE_PRESET = {
754
+ interface: {
755
+ self({ content }) {
756
+ return `export ${content}`;
757
+ },
758
+ property({ content }) {
759
+ return content;
760
+ },
761
+ },
762
+ };
763
+
764
+ /**
765
+ * Adding 'export' for union enumeration types
766
+ */
767
+ const EXPORT_UNION_TYPE_PRESET = {
768
+ type: {
769
+ async self({ renderer }) {
770
+ return `export ${await renderer.defaultSelf()}`;
771
+ },
772
+ },
773
+ };
774
+
775
+ /**
776
+ * Preset for TypeScriptGenerator, adds descriptions to types and properties.
777
+ */
778
+ const DESCRIPTION_PRESET = {
779
+ interface: {
780
+ self({ renderer, content, model }) {
781
+ const desc = model.getFromOriginalInput('description');
782
+ if (desc) {
783
+ const renderedDesc = renderer.renderComments(desc);
784
+ return `${renderedDesc}\n${content}`;
785
+ }
786
+ return content;
787
+ },
788
+ property({ renderer, model, content, propertyName }) {
789
+ if (model.getFromOriginalInput('properties') !== undefined) {
790
+ const property = model.getFromOriginalInput('properties')[propertyName];
791
+ if (property !== undefined) {
792
+ const desc = property['description'];
793
+ if (desc) {
794
+ const renderedDesc = renderer.renderComments(desc);
795
+ return `${renderedDesc}\n${content}`;
796
+ }
797
+ }
798
+ }
799
+ return content;
800
+ },
801
+ },
802
+ };
803
+
804
+ /**
805
+ * Preset for TypeScriptGenerator, additionalProperties added to models as "[k: string]: unknown;"
806
+ */
807
+ const ADDITIONAL_PROPERTIES_PRESET = {
808
+ interface: {
809
+ self({ content }) {
810
+ return content;
811
+ },
812
+ property({ renderer, propertyName, property, type }) {
813
+ if (type === PropertyType.additionalProperty) {
814
+ return property.originalInput === true
815
+ ? `\n[k: string]: unknown;`
816
+ : renderer
817
+ .renderProperty(propertyName, property)
818
+ .replace('additionalProperties?:', '[k: string]:');
819
+ }
820
+ return renderer.renderProperty(propertyName, property, type);
821
+ },
822
+ },
823
+ };
824
+
825
+ /**
826
+ * Preset for TypeScriptGenerator, adds imports to resulting model.
827
+ */
828
+ const IMPORTS_PRESET = {
829
+ interface: {
830
+ self({ content, model }) {
831
+ const interfaceName = model.getFromOriginalInput('title');
832
+ const interfacePrefix = getModelPathPrefix(interfaceName);
833
+ const dependencies = model.getNearestDependencies(model);
834
+ const imports = dependencies.map((dep) => ({
835
+ class: FormatHelpers.toPascalCase(dep),
836
+ location:
837
+ interfacePrefix !== getModelPathPrefix(dep)
838
+ ? `../${getModelPathPrefix(dep)}/${FormatHelpers.toParamCase(dep)}`
839
+ : `./${FormatHelpers.toParamCase(dep)}`,
840
+ }));
841
+ return endent`
842
+ ${imports
843
+ .sort((a, b) => a.location.localeCompare(b.location))
844
+ .map((i) => `import { ${i.class} } from '${i.location}';`)
845
+ .join('\n')}
846
+ ${content}`;
847
+ },
848
+ property({ content }) {
849
+ return content;
850
+ },
851
+ },
852
+ };
853
+
854
+ /**
855
+ * Preset for TypeScriptGenerator, sets nullable properties as unions in resulted models(compatibility with zapatos generated models).
856
+ */
857
+ const NULLABLE_PROPERTY_TO_UNION_PRESET = {
858
+ interface: {
859
+ self({ content }) {
860
+ return content;
861
+ },
862
+ property({ renderer, model, content, propertyName }) {
863
+ if (model.getFromOriginalInput('properties') !== undefined) {
864
+ const property = model.getFromOriginalInput('properties')[propertyName];
865
+ if (property !== undefined) {
866
+ if (Array.isArray(property['type'])) {
867
+ const renderProp = renderer.renderProperty(propertyName, property);
868
+ return renderProp.replace(
869
+ 'object',
870
+ content.split(':')[1].replace(';', ''),
871
+ );
872
+ }
873
+ }
874
+ }
875
+
876
+ return content;
877
+ },
878
+ },
879
+ };
880
+
881
+ /**
882
+ * Extension for @asyncapi/modelina, to make sure that the type name is in pascal.
883
+ */
884
+ const CustomNamingConvention: CommonNamingConvention = {
885
+ type: (name) => {
886
+ if (!name) {
887
+ return '';
888
+ }
889
+ return FormatHelpers.toPascalCase(name);
890
+ },
891
+ };