@xeno-js/cli 0.1.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 (172) hide show
  1. package/.github/.copilot-instruction.md +73 -0
  2. package/.github/agents/git-operator.agent.md +86 -0
  3. package/.github/agents/unit-tester.agent.md +68 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +1 -0
  6. package/.husky/pre-push +1 -0
  7. package/.prettierignore +7 -0
  8. package/.prettierrc.json +37 -0
  9. package/LICENSE +15 -0
  10. package/README.md +236 -0
  11. package/commitlint.config.cjs +39 -0
  12. package/dist/domain/contracts/icommand-cli.contracts.js +2 -0
  13. package/dist/domain/contracts/icommand-runner.contracts.js +2 -0
  14. package/dist/domain/contracts/idispatch.contracts.js +2 -0
  15. package/dist/domain/contracts/ifile-service.contracts.js +2 -0
  16. package/dist/domain/contracts/igenerator.contracts.js +2 -0
  17. package/dist/domain/contracts/index.js +2 -0
  18. package/dist/domain/contracts/iscaffold-strategy.contracts.js +2 -0
  19. package/dist/domain/index.js +2 -0
  20. package/dist/generator.contracts.js +13 -0
  21. package/dist/index.js +31 -0
  22. package/dist/infrastructure/bootstrap/bootstrapper.js +32 -0
  23. package/dist/infrastructure/bootstrap/index.js +17 -0
  24. package/dist/infrastructure/builder/builder.js +16 -0
  25. package/dist/infrastructure/builder/builder.test.js +30 -0
  26. package/dist/infrastructure/builder/index.js +17 -0
  27. package/dist/infrastructure/command_launcher/command.utils.js +30 -0
  28. package/dist/infrastructure/command_launcher/index.js +2 -0
  29. package/dist/infrastructure/commands/command-runner.service.js +36 -0
  30. package/dist/infrastructure/commands/generate.command.js +48 -0
  31. package/dist/infrastructure/commands/help.command.js +44 -0
  32. package/dist/infrastructure/commands/index.js +20 -0
  33. package/dist/infrastructure/commands/new-project.command.js +43 -0
  34. package/dist/infrastructure/dispatcher/dispatcher.js +39 -0
  35. package/dist/infrastructure/dispatcher/index.js +17 -0
  36. package/dist/infrastructure/file/file.service.js +13 -0
  37. package/dist/infrastructure/file/index.js +17 -0
  38. package/dist/infrastructure/generators/bootstrap.generator.js +118 -0
  39. package/dist/infrastructure/generators/core/bootstrap.generator.js +118 -0
  40. package/dist/infrastructure/generators/core/command/cqrs.generator.js +49 -0
  41. package/dist/infrastructure/generators/core/command/generate-command.generator.js +163 -0
  42. package/dist/infrastructure/generators/core/command/generate-query.generator.js +131 -0
  43. package/dist/infrastructure/generators/core/drizzle-sql-lite.generator.js +78 -0
  44. package/dist/infrastructure/generators/core/drizzle.generator.js +85 -0
  45. package/dist/infrastructure/generators/core/env.generator.js +69 -0
  46. package/dist/infrastructure/generators/core/index.js +25 -0
  47. package/dist/infrastructure/generators/core/main.generator.js +45 -0
  48. package/dist/infrastructure/generators/core/packagejson.generator.js +78 -0
  49. package/dist/infrastructure/generators/core/readme.generator.js +431 -0
  50. package/dist/infrastructure/generators/core/registry.generator.js +46 -0
  51. package/dist/infrastructure/generators/core/tsconfig.generator.js +41 -0
  52. package/dist/infrastructure/generators/core-packagejson.generator.js +78 -0
  53. package/dist/infrastructure/generators/drizzle-sql-lite.generator.js +78 -0
  54. package/dist/infrastructure/generators/drizzle.generator.js +85 -0
  55. package/dist/infrastructure/generators/env-vue.generator.js +40 -0
  56. package/dist/infrastructure/generators/env.generator.js +71 -0
  57. package/dist/infrastructure/generators/gitignore.generator.js +62 -0
  58. package/dist/infrastructure/generators/index.js +26 -0
  59. package/dist/infrastructure/generators/main.generator.js +45 -0
  60. package/dist/infrastructure/generators/readme.generator.js +431 -0
  61. package/dist/infrastructure/generators/registry.generator.js +46 -0
  62. package/dist/infrastructure/generators/tsconfig-vue.generator.js +39 -0
  63. package/dist/infrastructure/generators/tsconfig.generator.js +41 -0
  64. package/dist/infrastructure/generators/vite-config.generator.js +39 -0
  65. package/dist/infrastructure/generators/vue/app-vue.generator.js +26 -0
  66. package/dist/infrastructure/generators/vue/bootstrap-ts.generator.js +59 -0
  67. package/dist/infrastructure/generators/vue/command_query/cqrs.generator.js +36 -0
  68. package/dist/infrastructure/generators/vue/command_query/generate-command-vue.generator.js +139 -0
  69. package/dist/infrastructure/generators/vue/command_query/generate-query-vue.generator.js +147 -0
  70. package/dist/infrastructure/generators/vue/env.generator.js +40 -0
  71. package/dist/infrastructure/generators/vue/index-html.generator.js +29 -0
  72. package/dist/infrastructure/generators/vue/index.js +28 -0
  73. package/dist/infrastructure/generators/vue/main.generator.js +45 -0
  74. package/dist/infrastructure/generators/vue/packagejson.generator.js +59 -0
  75. package/dist/infrastructure/generators/vue/readme.generator.js +394 -0
  76. package/dist/infrastructure/generators/vue/registry-ts.generator.js +28 -0
  77. package/dist/infrastructure/generators/vue/router.generator.js +33 -0
  78. package/dist/infrastructure/generators/vue/tailwind.generator.js +19 -0
  79. package/dist/infrastructure/generators/vue/tsconfig.generator.js +39 -0
  80. package/dist/infrastructure/generators/vue/use-app.generator.js +43 -0
  81. package/dist/infrastructure/generators/vue/vite-config.generator.js +39 -0
  82. package/dist/infrastructure/generators/vue-packagejson.generator.js +59 -0
  83. package/dist/infrastructure/generators/vue-readme.generator.js +53 -0
  84. package/dist/infrastructure/generators/vue-source-files.generator.js +136 -0
  85. package/dist/infrastructure/index.js +21 -0
  86. package/dist/infrastructure/strategies/core-scaffold.strategy.js +99 -0
  87. package/dist/infrastructure/strategies/index.js +18 -0
  88. package/dist/infrastructure/strategies/vue-scaffold.strategy.js +79 -0
  89. package/dist/scaffolding.engine.js +60 -0
  90. package/dist/shared/constants/command.constants.js +19 -0
  91. package/dist/shared/constants/core.constants.js +10 -0
  92. package/dist/shared/constants/index.js +18 -0
  93. package/dist/shared/index.js +18 -0
  94. package/dist/shared/types/common.types.js +2 -0
  95. package/dist/shared/types/index.js +2 -0
  96. package/dist/shared/utils/guards.utils.js +310 -0
  97. package/dist/shared/utils/index.js +18 -0
  98. package/dist/shared/utils/string.utils.js +62 -0
  99. package/eslint.config.mjs +305 -0
  100. package/lint-staged.config.mjs +5 -0
  101. package/logo/logo.png +0 -0
  102. package/package.json +65 -0
  103. package/src/domain/contracts/icommand-cli.contracts.ts +13 -0
  104. package/src/domain/contracts/icommand-runner.contracts.ts +3 -0
  105. package/src/domain/contracts/idispatch.contracts.ts +3 -0
  106. package/src/domain/contracts/ifile-service.contracts.ts +3 -0
  107. package/src/domain/contracts/igenerator.contracts.ts +5 -0
  108. package/src/domain/contracts/index.ts +6 -0
  109. package/src/domain/contracts/iscaffold-strategy.contracts.ts +7 -0
  110. package/src/domain/index.ts +1 -0
  111. package/src/index.ts +30 -0
  112. package/src/infrastructure/bootstrap/bootstrapper.ts +30 -0
  113. package/src/infrastructure/bootstrap/index.ts +1 -0
  114. package/src/infrastructure/builder/builder.test.ts +36 -0
  115. package/src/infrastructure/builder/builder.ts +15 -0
  116. package/src/infrastructure/builder/index.ts +1 -0
  117. package/src/infrastructure/command_launcher/command.utils.ts +31 -0
  118. package/src/infrastructure/command_launcher/index.ts +0 -0
  119. package/src/infrastructure/commands/command-runner.service.ts +36 -0
  120. package/src/infrastructure/commands/generate.command.ts +55 -0
  121. package/src/infrastructure/commands/help.command.ts +63 -0
  122. package/src/infrastructure/commands/index.ts +4 -0
  123. package/src/infrastructure/commands/new-project.command.ts +47 -0
  124. package/src/infrastructure/dispatcher/dispatcher.ts +45 -0
  125. package/src/infrastructure/dispatcher/index.ts +1 -0
  126. package/src/infrastructure/file/file.service.ts +14 -0
  127. package/src/infrastructure/file/index.ts +1 -0
  128. package/src/infrastructure/generators/core/bootstrap.generator.ts +118 -0
  129. package/src/infrastructure/generators/core/command/cqrs.generator.ts +63 -0
  130. package/src/infrastructure/generators/core/command/generate-command.generator.ts +191 -0
  131. package/src/infrastructure/generators/core/command/generate-query.generator.ts +150 -0
  132. package/src/infrastructure/generators/core/drizzle-sql-lite.generator.ts +76 -0
  133. package/src/infrastructure/generators/core/drizzle.generator.ts +86 -0
  134. package/src/infrastructure/generators/core/env.generator.ts +69 -0
  135. package/src/infrastructure/generators/core/index.ts +9 -0
  136. package/src/infrastructure/generators/core/main.generator.ts +41 -0
  137. package/src/infrastructure/generators/core/packagejson.generator.ts +70 -0
  138. package/src/infrastructure/generators/core/readme.generator.ts +427 -0
  139. package/src/infrastructure/generators/core/registry.generator.ts +40 -0
  140. package/src/infrastructure/generators/core/tsconfig.generator.ts +39 -0
  141. package/src/infrastructure/generators/gitignore.generator.ts +60 -0
  142. package/src/infrastructure/generators/vue/app-vue.generator.ts +21 -0
  143. package/src/infrastructure/generators/vue/bootstrap-ts.generator.ts +60 -0
  144. package/src/infrastructure/generators/vue/command_query/cqrs.generator.ts +36 -0
  145. package/src/infrastructure/generators/vue/command_query/generate-command-vue.generator.ts +153 -0
  146. package/src/infrastructure/generators/vue/command_query/generate-query-vue.generator.ts +161 -0
  147. package/src/infrastructure/generators/vue/env.generator.ts +39 -0
  148. package/src/infrastructure/generators/vue/index-html.generator.ts +24 -0
  149. package/src/infrastructure/generators/vue/index.ts +12 -0
  150. package/src/infrastructure/generators/vue/main.generator.ts +41 -0
  151. package/src/infrastructure/generators/vue/packagejson.generator.ts +50 -0
  152. package/src/infrastructure/generators/vue/readme.generator.ts +389 -0
  153. package/src/infrastructure/generators/vue/registry-ts.generator.ts +26 -0
  154. package/src/infrastructure/generators/vue/router.generator.ts +35 -0
  155. package/src/infrastructure/generators/vue/tailwind.generator.ts +17 -0
  156. package/src/infrastructure/generators/vue/tsconfig.generator.ts +38 -0
  157. package/src/infrastructure/generators/vue/use-app.generator.ts +38 -0
  158. package/src/infrastructure/generators/vue/vite-config.generator.ts +35 -0
  159. package/src/infrastructure/index.ts +5 -0
  160. package/src/infrastructure/strategies/core-scaffold.strategy.ts +100 -0
  161. package/src/infrastructure/strategies/index.ts +2 -0
  162. package/src/infrastructure/strategies/vue-scaffold.strategy.ts +79 -0
  163. package/src/shared/constants/command.constants.ts +18 -0
  164. package/src/shared/constants/core.constants.ts +7 -0
  165. package/src/shared/constants/index.ts +2 -0
  166. package/src/shared/index.ts +3 -0
  167. package/src/shared/types/common.types.ts +68 -0
  168. package/src/shared/types/index.ts +1 -0
  169. package/src/shared/utils/guards.utils.ts +334 -0
  170. package/src/shared/utils/index.ts +2 -0
  171. package/src/shared/utils/string.utils.ts +60 -0
  172. package/tsconfig.json +15 -0
@@ -0,0 +1,118 @@
1
+ import path from 'node:path'
2
+
3
+ import { type IFileService, type IGenerator } from '../../../domain'
4
+ import { CORE_CONSTANTS, type CoreOptions } from '../../../shared'
5
+
6
+ export class BootstrapGenerator implements IGenerator<CoreOptions> {
7
+ constructor(private readonly _fileService: IFileService) {}
8
+
9
+ public async generate(projectPath: string, options: CoreOptions): Promise<void> {
10
+ // Costruisce in modo flessibile i nodi di configurazione
11
+ let builderNodes = `
12
+ const builder = new AppBuilder<MyRegistry>()
13
+ .addContext()
14
+ .addMiddlewares((opts) => {
15
+ // Add your options here
16
+ })
17
+ .addPipeline((opts) => {
18
+ // Add your options here
19
+ })`
20
+
21
+ if (options.database !== CORE_CONSTANTS.NONE) {
22
+ if (options.database === CORE_CONSTANTS.DRIZZLE) {
23
+ builderNodes += `
24
+ .addDb((opts, config) => {
25
+ opts.enableSqlLite = false
26
+ opts.connectionString = config.getOrThrow('DATABASE_URL');
27
+ })`
28
+ } else if (options.database === CORE_CONSTANTS.SQL_LITE) {
29
+ builderNodes += `
30
+ .addDb((opts, config) => {
31
+ opts.enableSqlLite = true;
32
+ opts.connectionString = config.getOrThrow('SQLITE_DATABASE_URL');
33
+ })`
34
+ }
35
+ }
36
+
37
+ if (options.redis) {
38
+ builderNodes += `
39
+ .addCache((opts, config) => {
40
+ const isDev = config.get('NODE_ENV') === 'development';
41
+ opts.inMemory = isDev;
42
+ opts.redis = !isDev ? {
43
+ host: config.getOrThrow('REDIS_HOST'),
44
+ port: config.getNumber('REDIS_PORT') ?? 6379,
45
+ username: config.get('REDIS_USERNAME') ?? '',
46
+ password: config.get('REDIS_PASSWORD') ?? '',
47
+ tls: config.getBoolean('REDIS_TLS') ?? false,
48
+ maxRetriesPerRequest: 3
49
+ } : undefined
50
+ })`
51
+ } else {
52
+ // Default in-memory cache if no redis selected but CQRS pipeline exists
53
+ builderNodes += `
54
+ .addCache((opts) => {
55
+ opts.inMemory = true;
56
+ })`
57
+ }
58
+
59
+ if (options.supabase) {
60
+ builderNodes += `
61
+ .addAuth((opts, config) => {
62
+ opts.url = config.getOrThrow('SUPABASE_URL');
63
+ opts.key = config.getOrThrow('SUPABASE_KEY');
64
+ })`
65
+ }
66
+
67
+ if (options.pino || options.sentry) {
68
+ const pinoConfig = options.pino
69
+ ? `opts.pino.config = !isDev ? { env: config.getOrThrow('NODE_ENV'), destination: 'stdout', prettyPrint: false } : undefined;`
70
+ : ''
71
+ const sentryConfig = options.sentry
72
+ ? `opts.sentry.config = !isDev ? { dsn: config.getOrThrow('SENTRY_DSN'), environment: config.getOrThrow('NODE_ENV') } : undefined;`
73
+ : ''
74
+
75
+ builderNodes += `
76
+ .addLogger((opts, config) => {
77
+ const isDev = config.get('NODE_ENV') === 'development';
78
+ opts.level = LOG_LEVEL.INFO;
79
+ opts.console = isDev;
80
+ ${pinoConfig}
81
+ ${sentryConfig}
82
+ })`
83
+ }
84
+
85
+ builderNodes += `
86
+ .addServices((opts, config) => {
87
+ // Register custom repositories and handlers here
88
+ });`
89
+
90
+ const content = `import type { IServiceContainer } from '@xeno-js/core';
91
+ import { AppBuilder, LOG_LEVEL, Nullable, TOKENS } from '@xeno-js/core';
92
+ import type { MyRegistry } from './registry';
93
+
94
+ let containerPromise: Nullable<Promise<IServiceContainer<MyRegistry>>> = null;
95
+
96
+ export async function bootstrap() {
97
+ if (!containerPromise) {
98
+ containerPromise = (async () => {
99
+ const builder = getContainer();
100
+ return await builder.build();
101
+ })().catch((err) => {
102
+ containerPromise = null;
103
+ throw err;
104
+ });
105
+ }
106
+ return containerPromise;
107
+ }
108
+
109
+ function getContainer() {
110
+ ${builderNodes}
111
+ return builder;
112
+ }
113
+ `
114
+
115
+ const filePath = path.join(projectPath, 'src', 'bootstrap.ts')
116
+ await this._fileService.writeFileRecursive(filePath, content)
117
+ }
118
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import type { IFileService } from '../../../../domain'
5
+ import { COMMAND_CONSTANTS, Guards, StringUtils } from '../../../../shared'
6
+
7
+ export class CqrsGenerator {
8
+ constructor(private readonly _fileService: IFileService) {}
9
+
10
+ public async generate(
11
+ targetDir: string,
12
+ name: string,
13
+ type: 'command' | 'query',
14
+ outputPath?: string,
15
+ ): Promise<void> {
16
+ const pascalName = StringUtils.pascalCase(name)
17
+ const tokenPrefix = StringUtils.splitCamelCaseWithUnderscore(pascalName).toUpperCase()
18
+ const lowerName = name.toLowerCase()
19
+ const destinationFolder = outputPath ?? lowerName
20
+ const componentDir = path.join(targetDir, 'src', destinationFolder)
21
+
22
+ let hasZod = false
23
+ let hasDrizzle = false
24
+
25
+ try {
26
+ const pkgJsonStr = await fs.readFile(path.join(targetDir, 'package.json'), 'utf8')
27
+ const pkgJson = JSON.parse(pkgJsonStr) as {
28
+ dependencies?: Record<string, string>
29
+ devDependencies?: Record<string, string>
30
+ }
31
+ const deps = { ...(pkgJson.dependencies ?? {}), ...(pkgJson.devDependencies ?? {}) }
32
+ hasZod = !!Guards.isDefined(deps['zod'])
33
+ hasDrizzle = !!Guards.isDefined(deps['drizzle-orm'])
34
+ } catch (error) {
35
+ console.warn(
36
+ ' Warning: Could not read package.json. Assuming Zod and Drizzle are not installed.',
37
+ error,
38
+ )
39
+ }
40
+
41
+ switch (type) {
42
+ case COMMAND_CONSTANTS.COMMAND: {
43
+ const { GenerateCommandCoreGenerator } = await import('./generate-command.generator.js')
44
+ const generator = new GenerateCommandCoreGenerator(this._fileService)
45
+ return generator.generate(
46
+ pascalName,
47
+ lowerName,
48
+ tokenPrefix,
49
+ componentDir,
50
+ hasZod,
51
+ hasDrizzle,
52
+ )
53
+ }
54
+ case COMMAND_CONSTANTS.QUERY: {
55
+ const { GenerateQueryCoreGenerator } = await import('./generate-query.generator.js')
56
+ const generatorQuery = new GenerateQueryCoreGenerator(this._fileService)
57
+ return generatorQuery.generate(pascalName, lowerName, tokenPrefix, componentDir, hasZod)
58
+ }
59
+ default:
60
+ throw new Error(`Unsupported generator type: '${String(type)}'. Use 'command' or 'query'.`)
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,191 @@
1
+ import path from 'node:path'
2
+
3
+ import type { IFileService } from '../../../../domain'
4
+
5
+ export class GenerateCommandCoreGenerator {
6
+ constructor(private readonly _fileService: IFileService) {}
7
+
8
+ public async generate(
9
+ pascalName: string,
10
+ lowerName: string,
11
+ tokenPrefix: string,
12
+ componentDir: string,
13
+ hasZod: boolean,
14
+ hasDrizzle: boolean,
15
+ ): Promise<void> {
16
+ // 1. Command
17
+ const commandContent = `import { Command } from '@xeno-js/core';
18
+
19
+ /*
20
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
21
+ * Please replace <any> with your specific payload and response types.
22
+ */
23
+ export class ${pascalName}Command extends Command<any> {
24
+ constructor(public readonly payload: any) {
25
+ super('${tokenPrefix}_COMMAND_HANDLER');
26
+ }
27
+ }
28
+ `
29
+ await this._fileService.writeFileRecursive(
30
+ path.join(componentDir, `${lowerName}.command.ts`),
31
+ commandContent,
32
+ )
33
+
34
+ // 2. Handler
35
+ const handlerContent = `import type { IFactory, ResultType, UserContext } from '@xeno-js/core';
36
+ import { AppError, BaseHandler, Result } from '@xeno-js/core';
37
+ import { ${pascalName}Command } from './${lowerName}.command';
38
+
39
+ /*
40
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
41
+ * Please replace <any> with your specific response type.
42
+ */
43
+ export class ${pascalName}Handler extends BaseHandler<${pascalName}Command, any> {
44
+ constructor(
45
+ identityFactory: IFactory<void, UserContext>
46
+ ) {
47
+ super(identityFactory);
48
+ }
49
+
50
+ public async executeAsync(request: ${pascalName}Command, singal: AbortSignal): Promise<ResultType<any>> {
51
+ // TODO: Implement your business logic here
52
+
53
+ return Result.ok();
54
+ }
55
+ }
56
+ `
57
+ await this._fileService.writeFileRecursive(
58
+ path.join(componentDir, `${lowerName}.handler.ts`),
59
+ handlerContent,
60
+ )
61
+
62
+ // 3. Controller
63
+ const controllerContent = `import type { IContextAccessor, IMediator, RequestContext, ResponseDto } from '@xeno-js/core';
64
+ import { BaseController } from '@xeno-js/core';
65
+ import { ${pascalName}Command } from './${lowerName}.command';
66
+
67
+ /*
68
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
69
+ * Please replace <any, any> with your specific request and response types.
70
+ */
71
+ export class ${pascalName}Controller extends BaseController<any, any> {
72
+ constructor(
73
+ requestContext: IContextAccessor<RequestContext>,
74
+ mediator: IMediator,
75
+ ) {
76
+ super(requestContext, mediator);
77
+ }
78
+
79
+ public async handle(request: any): Promise<ResponseDto<any>> {
80
+ const cmd = new ${pascalName}Command(request);
81
+ const result = await this._send(cmd);
82
+
83
+ if (!result.isOk()) {
84
+ return this.fail(result.getErrorOrThrow(), 'Error during ${pascalName} operation');
85
+ }
86
+
87
+ return this.ok(result.getValueOrThrow());
88
+ }
89
+ }
90
+ `
91
+ await this._fileService.writeFileRecursive(
92
+ path.join(componentDir, `${lowerName}.controller.ts`),
93
+ controllerContent,
94
+ )
95
+
96
+ // 4. Entity
97
+ const entityContent = `import { Entity } from '@xeno-js/core';
98
+
99
+ export interface ${pascalName}Props {
100
+ // TODO: Define your entity properties
101
+ }
102
+
103
+ /*
104
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
105
+ * Adjust the properties and types according to your domain logic.
106
+ */
107
+ export class ${pascalName} extends Entity<${pascalName}Props> {
108
+ private constructor(props: ${pascalName}Props, id?: string) {
109
+ super(props, id);
110
+ }
111
+
112
+ static create(props: ${pascalName}Props, id?: string): ${pascalName} {
113
+ return new ${pascalName}(props, id);
114
+ }
115
+ }
116
+ `
117
+ await this._fileService.writeFileRecursive(
118
+ path.join(componentDir, `${lowerName}.entity.ts`),
119
+ entityContent,
120
+ )
121
+
122
+ // 5. Schema Zod (Condizionale)
123
+ if (hasZod) {
124
+ const zodContent = `import { z } from 'zod';
125
+ import { ZodUtils } from '@xeno-js/core';
126
+
127
+ /*
128
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
129
+ * Define the actual Zod validation schema for your command payload.
130
+ */
131
+ export const ${pascalName}Schema = ZodUtils.createCommandSchema({
132
+ payload: z.any() // TODO: Update with strict validation
133
+ });
134
+ `
135
+ await this._fileService.writeFileRecursive(
136
+ path.join(componentDir, `${lowerName}.schema-zod.ts`),
137
+ zodContent,
138
+ )
139
+ }
140
+
141
+ // 6. Schema DB (Condizionale)
142
+ if (hasDrizzle) {
143
+ const dbContent = `import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
144
+ // Note: If you are using SQLite, change the import to 'drizzle-orm/sqlite-core' and update the table definition.
145
+
146
+ /*
147
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
148
+ * Update the schema definition with your actual database columns.
149
+ */
150
+ export const ${lowerName}s = pgTable('${lowerName}s', {
151
+ id: text('id').primaryKey(),
152
+ createdAt: timestamp('created_at').defaultNow(),
153
+ updatedAt: timestamp('updated_at').defaultNow().$onUpdate(() => new Date()),
154
+ });
155
+
156
+ export type ${pascalName}Dto = typeof ${lowerName}s.$inferSelect;
157
+ export type New${pascalName}Dto = typeof ${lowerName}s.$inferInsert;
158
+ `
159
+ await this._fileService.writeFileRecursive(
160
+ path.join(componentDir, `${lowerName}.schema-db.ts`),
161
+ dbContent,
162
+ )
163
+ }
164
+
165
+ // 7. Module
166
+ const moduleContent = `import type { IServiceContainer, IConfigurationService } from '@xeno-js/core';
167
+ import { TOKENS } from '@xeno-js/core';
168
+ import { ${pascalName}Controller } from './${lowerName}.controller';
169
+ import { ${pascalName}Handler } from './${lowerName}.handler';
170
+
171
+ export const ${pascalName}Module = Object.freeze({
172
+ register(opts: IServiceContainer<any>, _config: IConfigurationService): void {
173
+ opts.addTransient('${tokenPrefix}_COMMAND_CONTROLLER', (c) => new ${pascalName}Controller(c.resolve(TOKENS.REQUEST_CONTEXT), c.resolve(TOKENS.MEDIATOR)));
174
+ opts.addScoped('${tokenPrefix}_COMMAND_HANDLER', (c) => new ${pascalName}Handler(c.resolve(TOKENS.USER_CONTEXT_FACTORY)));
175
+
176
+ /*
177
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
178
+ * Please copy and paste the following tokens to your MyRegistry interface (usually in src/registry.ts):
179
+ *
180
+ * ${tokenPrefix}_COMMAND_CONTROLLER: IController<any, any>;
181
+ * ${tokenPrefix}_COMMAND_HANDLER: IHandler<${pascalName}Command, any>;
182
+ */
183
+ }
184
+ } as const);
185
+ `
186
+ await this._fileService.writeFileRecursive(
187
+ path.join(componentDir, `${lowerName}.module.ts`),
188
+ moduleContent,
189
+ )
190
+ }
191
+ }
@@ -0,0 +1,150 @@
1
+ import path from 'node:path'
2
+
3
+ import type { IFileService } from '../../../../domain'
4
+
5
+ export class GenerateQueryCoreGenerator {
6
+ constructor(private readonly _fileService: IFileService) {}
7
+
8
+ public async generate(
9
+ pascalName: string,
10
+ lowerName: string,
11
+ tokenPrefix: string,
12
+ componentDir: string,
13
+ hasZod: boolean,
14
+ ): Promise<void> {
15
+ const queryContent = `import { BaseQuery } from '@xeno-js/core';
16
+
17
+ /*
18
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
19
+ * Please replace <any> with your specific payload and response types.
20
+ */
21
+ export class ${pascalName}Query extends BaseQuery<any> {
22
+ constructor(public readonly payload: any) {
23
+ super(
24
+ '${tokenPrefix}_QUERY_HANDLER',
25
+ // Default cache options:
26
+ {
27
+ ttl: 60,
28
+ cacheKey: \`${lowerName}:\${JSON.stringify(payload)}\`,
29
+ bypassCache: false,
30
+ consistentRead: false
31
+ isUserScoped: false,
32
+ }
33
+ );
34
+ }
35
+ }
36
+ `
37
+ await this._fileService.writeFileRecursive(
38
+ path.join(componentDir, `${lowerName}.query.ts`),
39
+ queryContent,
40
+ )
41
+
42
+ // 2. Handler
43
+ const handlerContent = `import type { IFactory, ResultType, UserContext } from '@xeno-js/core';
44
+ import { AppError, BaseHandler, Result } from '@xeno-js/core';
45
+ import { ${pascalName}Query } from './${lowerName}.query';
46
+
47
+ /*
48
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
49
+ * Please replace <any> with your specific response type.
50
+ */
51
+ export class ${pascalName}Handler extends BaseHandler<${pascalName}Query, any> {
52
+ constructor(
53
+ identityFactory: IFactory<void, UserContext>
54
+ // TODO: Inject your ReadDao or DataSource here
55
+ ) {
56
+ super(identityFactory);
57
+ }
58
+
59
+ public async executeAsync(request: ${pascalName}Query, singal: AbortSignal): Promise<ResultType<any>> {
60
+ // TODO: Implement your query logic here
61
+
62
+ return Result.ok();
63
+ }
64
+ }
65
+ `
66
+ await this._fileService.writeFileRecursive(
67
+ path.join(componentDir, `${lowerName}.handler.ts`),
68
+ handlerContent,
69
+ )
70
+
71
+ // 3. Controller
72
+ const controllerContent = `import type { IContextAccessor, IMediator, RequestContext, ResponseDto } from '@xeno-js/core';
73
+ import { BaseController } from '@xeno-js/core';
74
+ import { ${pascalName}Query } from './${lowerName}.query';
75
+
76
+ /*
77
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
78
+ * Please replace <any, any> with your specific request and response types.
79
+ */
80
+ export class ${pascalName}Controller extends BaseController<any, any> {
81
+ constructor(
82
+ requestContext: IContextAccessor<RequestContext>,
83
+ mediator: IMediator,
84
+ ) {
85
+ super(requestContext, mediator);
86
+ }
87
+
88
+ public async handle(request: any): Promise<ResponseDto<any>> {
89
+ const query = new ${pascalName}Query(request);
90
+ const result = await this._query(query);
91
+
92
+ if (!result.isOk()) {
93
+ return this.fail(result.getErrorOrThrow(), 'Error during ${pascalName} operation');
94
+ }
95
+
96
+ return this.ok(result.getValueOrThrow());
97
+ }
98
+ }
99
+ `
100
+ await this._fileService.writeFileRecursive(
101
+ path.join(componentDir, `${lowerName}.controller.ts`),
102
+ controllerContent,
103
+ )
104
+
105
+ // 4. Schema Zod
106
+ if (hasZod) {
107
+ const zodContent = `import { z } from 'zod';
108
+ import { ZodUtils } from '@xeno-js/core';
109
+
110
+ /*
111
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
112
+ * Define the actual Zod validation schema for your query payload.
113
+ */
114
+ export const ${pascalName}Schema = ZodUtils.createQuerySchema({
115
+ payload: z.any() // TODO: Update with strict validation
116
+ });
117
+ `
118
+ await this._fileService.writeFileRecursive(
119
+ path.join(componentDir, `${lowerName}.schema-zod.ts`),
120
+ zodContent,
121
+ )
122
+ }
123
+
124
+ // 5. Module
125
+ const moduleContent = `import type { IServiceContainer, IConfigurationService } from '@xeno-js/core';
126
+ import { TOKENS } from '@xeno-js/core';
127
+ import { ${pascalName}Controller } from './${lowerName}.controller';
128
+ import { ${pascalName}Handler } from './${lowerName}.handler';
129
+
130
+ export const ${pascalName}Module = Object.freeze({
131
+ register(opts: IServiceContainer<any>, _config: IConfigurationService): void {
132
+ opts.addTransient('${tokenPrefix}_QUERY_CONTROLLER', (c) => new ${pascalName}Controller(c.resolve(TOKENS.REQUEST_CONTEXT), c.resolve(TOKENS.MEDIATOR)));
133
+ opts.addScoped('${tokenPrefix}_QUERY_HANDLER', (c) => new ${pascalName}Handler(c.resolve(TOKENS.USER_CONTEXT_FACTORY)));
134
+
135
+ /*
136
+ * ⚠️ WARNING: ACTION REQUIRED ⚠️
137
+ * Please copy and paste the following tokens to your MyRegistry interface (usually in src/registry.ts):
138
+ *
139
+ * ${tokenPrefix}_QUERY_CONTROLLER: IController<any, any>;
140
+ * ${tokenPrefix}_QUERY_HANDLER: IHandler<${pascalName}Query, any>;
141
+ */
142
+ }
143
+ } as const);
144
+ `
145
+ await this._fileService.writeFileRecursive(
146
+ path.join(componentDir, `${lowerName}.module.ts`),
147
+ moduleContent,
148
+ )
149
+ }
150
+ }
@@ -0,0 +1,76 @@
1
+ import path from 'node:path'
2
+
3
+ import type { IFileService, IGenerator } from '../../../domain'
4
+ import { type CoreOptions } from '../../../shared'
5
+
6
+ /**
7
+ * @class DrizzleSqlLiteGenerator
8
+ * @description Generates a Drizzle configuration file (drizzle.config.ts) and a
9
+ * SQLite schema example (src/infrastructure/db/sqlite-schema.ts) when the user
10
+ * opts to use SQLite/libSQL instead of Postgres.
11
+ */
12
+ export class DrizzleSqlLiteGenerator implements IGenerator<CoreOptions> {
13
+ constructor(private readonly _fileService: IFileService) {}
14
+
15
+ async generate(projectPath: string, _options: CoreOptions): Promise<void> {
16
+ const configContent = this.composeDrizzleConfig()
17
+ const filePath = path.join(projectPath, 'drizzle.config.ts')
18
+ await this._fileService.writeFileRecursive(filePath, configContent)
19
+
20
+ const schemaPath = path.join(projectPath, 'src', 'schema.ts')
21
+ const schemaContent = this.composeSchema()
22
+ await this._fileService.writeFileRecursive(schemaPath, schemaContent)
23
+ }
24
+
25
+ private composeDrizzleConfig(): string {
26
+ return `import 'dotenv/config';
27
+ import { defineConfig } from 'drizzle-kit';
28
+
29
+ /**
30
+ * Drizzle Kit configuration for SQLite / libSQL.
31
+ * Uses environment variable SQLITE_DATABASE_URL to locate the database.
32
+ */
33
+ export default defineConfig({
34
+ schema: './src/schema.ts',
35
+ out: './drizzle',
36
+ dialect: 'sqlite',
37
+ dbCredentials: {
38
+ url: process.env.SQLITE_DATABASE_URL!,
39
+ },
40
+ verbose: true,
41
+ strict: true,
42
+ });
43
+ `
44
+ }
45
+
46
+ private composeSchema(): string {
47
+ return `import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
48
+
49
+ /**
50
+ * Example SQLite schema using Drizzle's libsql primitives.
51
+ * Place your application tables here (this file is scaffolded by the CLI when
52
+ * the user opts into SQLite support).
53
+ */
54
+ export const usersTable = sqliteTable('users', {
55
+ id: integer('id').primaryKey().notNull(),
56
+ name: text('name').notNull(),
57
+ description: text('description'),
58
+ });
59
+
60
+ /**
61
+ * Types inferred from the schema.
62
+ * - UserDto: Represents the shape of the data retrieved from the database ($inferSelect).
63
+ * - NewUserDto: Represents the shape of the data required to insert a new record ($inferInsert).
64
+ */
65
+ export type UserDto = typeof usersTable.$inferSelect;
66
+ export type NewUserDto = typeof usersTable.$inferInsert;
67
+
68
+ /**
69
+ * Centralized Database Schema dictionary.
70
+ */
71
+ export type SqliteSchema = {
72
+ users: typeof usersTable;
73
+ };
74
+ `
75
+ }
76
+ }
@@ -0,0 +1,86 @@
1
+ import path from 'node:path'
2
+
3
+ import type { IFileService, IGenerator } from '../../../domain'
4
+ import { type CoreOptions } from '../../../shared'
5
+
6
+ /**
7
+ * @class DrizzleGenerator
8
+ * @description Generates the Drizzle configuration file (drizzle.config.ts) and a basic schema file (src/schema.ts) for database scaffolding.
9
+ * The generator is only invoked if the user has opted to include database support in their project.
10
+ *
11
+ * @author Xeno
12
+ * @version 1.0.0
13
+ * @license ISC
14
+ * @since 2025-09-30
15
+ * @link https://github.com/xeno-js/xeno-js
16
+ */
17
+ export class DrizzleGenerator implements IGenerator<CoreOptions> {
18
+ constructor(private readonly _fileService: IFileService) {}
19
+
20
+ async generate(projectPath: string, _options: CoreOptions): Promise<void> {
21
+ const configContent = this.composeDrizzleConfig()
22
+ const filePath = path.join(projectPath, 'drizzle.config.ts')
23
+ await this._fileService.writeFileRecursive(filePath, configContent)
24
+
25
+ const schemaContent = this.composeSchema()
26
+ await this._fileService.writeFileRecursive(
27
+ path.join(projectPath, 'src', 'schema.ts'),
28
+ schemaContent,
29
+ )
30
+ }
31
+
32
+ private composeDrizzleConfig(): string {
33
+ return `import 'dotenv/config';
34
+ import { defineConfig } from 'drizzle-kit';
35
+
36
+ /**
37
+ * Drizzle Kit configuration file.
38
+ * Defines the schema location, migration output directory, and database connection.
39
+ */
40
+ export default defineConfig({
41
+ schema: './src/schema.ts',
42
+ out: './drizzle',
43
+ dialect: 'postgresql',
44
+ dbCredentials: {
45
+ url: process.env.DATABASE_URL!,
46
+ },
47
+ verbose: true,
48
+ strict: true,
49
+ });
50
+ `
51
+ }
52
+
53
+ private composeSchema(): string {
54
+ return `import { boolean, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
55
+
56
+ /**
57
+ * @description Define your database tables here.
58
+ * The 'usersTable' serves as a baseline example.
59
+ * You can add more tables, relations, and indexes according to your domain requirements.
60
+ */
61
+ export const usersTable = pgTable('users', {
62
+ id: serial('id').primaryKey(),
63
+ name: text('name').notNull(),
64
+ email: text('email').notNull().unique(),
65
+ isDeleted: boolean('is_deleted').notNull().default(false),
66
+ deletedAt: timestamp('deleted_at'),
67
+ createdAt: timestamp('created_at').defaultNow(),
68
+ });
69
+
70
+ /**
71
+ * Types inferred from the schema.
72
+ * - UserDto: Represents the shape of the data retrieved from the database ($inferSelect).
73
+ * - NewUserDto: Represents the shape of the data required to insert a new record ($inferInsert).
74
+ */
75
+ export type UserDto = typeof usersTable.$inferSelect;
76
+ export type NewUserDto = typeof usersTable.$inferInsert;
77
+
78
+ /**
79
+ * Centralized Database Schema dictionary.
80
+ */
81
+ export type DbSchema = {
82
+ users: typeof usersTable;
83
+ };
84
+ `
85
+ }
86
+ }