@exium/cli 1.0.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.
- package/LICENSE.md +21 -0
- package/README.md +23 -0
- package/dist/index.js +698 -0
- package/package.json +55 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Exium Framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Exium CLI
|
|
2
|
+
|
|
3
|
+
CLI for scaffolding Exium microservices.
|
|
4
|
+
|
|
5
|
+
Install globally:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @exium/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Create a service:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
exium new order-management
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Create a service with an explicit aggregate/entity name:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
exium new order-management --entity Order
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The generated service depends on `@exium/core` and starts with a default aggregate, application handlers, MikroORM persistence adapter, and external HTTP controller.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/new.ts
|
|
30
|
+
var import_path = __toESM(require("path"));
|
|
31
|
+
var import_fs = __toESM(require("fs"));
|
|
32
|
+
var import_child_process = require("child_process");
|
|
33
|
+
var import_chalk = __toESM(require("chalk"));
|
|
34
|
+
var import_ora = __toESM(require("ora"));
|
|
35
|
+
|
|
36
|
+
// src/utils/names.ts
|
|
37
|
+
function toKebab(str) {
|
|
38
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
function toPascal(str) {
|
|
41
|
+
return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
42
|
+
}
|
|
43
|
+
function pluralize(str) {
|
|
44
|
+
if (str.endsWith("y") && !/[aeiou]y$/.test(str)) {
|
|
45
|
+
return str.slice(0, -1) + "ies";
|
|
46
|
+
}
|
|
47
|
+
if (/s|sh|ch|x|z$/.test(str)) return str + "es";
|
|
48
|
+
return str + "s";
|
|
49
|
+
}
|
|
50
|
+
function buildNames(serviceName, entityName) {
|
|
51
|
+
const svc = toKebab(serviceName);
|
|
52
|
+
const ent = toKebab(entityName);
|
|
53
|
+
return {
|
|
54
|
+
serviceName: svc,
|
|
55
|
+
ServiceName: toPascal(svc),
|
|
56
|
+
entityName: ent,
|
|
57
|
+
EntityName: toPascal(ent),
|
|
58
|
+
entityNames: pluralize(ent),
|
|
59
|
+
EntityNames: toPascal(pluralize(ent))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/templates/config.ts
|
|
64
|
+
function packageJson(n) {
|
|
65
|
+
return JSON.stringify(
|
|
66
|
+
{
|
|
67
|
+
name: n.serviceName,
|
|
68
|
+
version: "0.0.1",
|
|
69
|
+
private: true,
|
|
70
|
+
scripts: {
|
|
71
|
+
build: "tsc -p tsconfig.json",
|
|
72
|
+
"build:watch": "tsc -p tsconfig.json --watch",
|
|
73
|
+
start: "node dist/main.js",
|
|
74
|
+
"start:dev": "ts-node -r tsconfig-paths/register src/main.ts"
|
|
75
|
+
},
|
|
76
|
+
dependencies: {
|
|
77
|
+
"@exium/core": "^1.0.0-rc.0",
|
|
78
|
+
"@nestjs/common": "^11.0.0",
|
|
79
|
+
"@nestjs/core": "^11.0.0",
|
|
80
|
+
"@nestjs/config": "^4.0.0",
|
|
81
|
+
"@nestjs/platform-express": "^11.0.0",
|
|
82
|
+
"@mikro-orm/core": "^7.0.0",
|
|
83
|
+
"@mikro-orm/mongodb": "^7.0.0",
|
|
84
|
+
"@mikro-orm/nestjs": "^7.0.0",
|
|
85
|
+
"@mikro-orm/postgresql": "^7.0.0",
|
|
86
|
+
"class-validator": "^0.15.0",
|
|
87
|
+
"class-transformer": "^0.5.0",
|
|
88
|
+
"nestjs-pino": "^4.6.0",
|
|
89
|
+
pino: "^10.0.0",
|
|
90
|
+
"pino-http": "^11.0.0",
|
|
91
|
+
"pino-pretty": "^13.0.0",
|
|
92
|
+
"reflect-metadata": "^0.2.0",
|
|
93
|
+
rxjs: "^7.8.0",
|
|
94
|
+
uuid: "^14.0.0"
|
|
95
|
+
},
|
|
96
|
+
devDependencies: {
|
|
97
|
+
"@types/node": "^22.0.0",
|
|
98
|
+
"ts-node": "^10.9.0",
|
|
99
|
+
"tsconfig-paths": "^4.2.0",
|
|
100
|
+
typescript: "^6.0.0"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function tsConfig() {
|
|
108
|
+
return JSON.stringify(
|
|
109
|
+
{
|
|
110
|
+
compilerOptions: {
|
|
111
|
+
target: "ES2022",
|
|
112
|
+
module: "NodeNext",
|
|
113
|
+
moduleResolution: "NodeNext",
|
|
114
|
+
lib: ["ES2022"],
|
|
115
|
+
outDir: "dist",
|
|
116
|
+
rootDir: "src",
|
|
117
|
+
strict: true,
|
|
118
|
+
ignoreDeprecations: "6.0",
|
|
119
|
+
strictNullChecks: true,
|
|
120
|
+
esModuleInterop: true,
|
|
121
|
+
allowSyntheticDefaultImports: true,
|
|
122
|
+
experimentalDecorators: true,
|
|
123
|
+
emitDecoratorMetadata: true,
|
|
124
|
+
skipLibCheck: true,
|
|
125
|
+
declaration: true,
|
|
126
|
+
declarationMap: true,
|
|
127
|
+
sourceMap: true,
|
|
128
|
+
resolveJsonModule: true,
|
|
129
|
+
paths: { "@/*": ["./src/*"] }
|
|
130
|
+
},
|
|
131
|
+
include: ["src"],
|
|
132
|
+
exclude: ["node_modules", "dist"]
|
|
133
|
+
},
|
|
134
|
+
null,
|
|
135
|
+
2
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
function envExample(n) {
|
|
139
|
+
return `# ${n.ServiceName} Service \u2014 Ortam De\u011Fi\u015Fkenleri
|
|
140
|
+
|
|
141
|
+
EXIUM_DB_DRIVER=postgres
|
|
142
|
+
EXIUM_DB_URL=postgresql://postgres:postgres@localhost:5432/${n.serviceName.replace(/-/g, "_")}
|
|
143
|
+
|
|
144
|
+
# Opsiyonel
|
|
145
|
+
# EXIUM_RABBITMQ_URL=amqp://localhost:5672
|
|
146
|
+
# EXIUM_REDIS_URL=redis://localhost:6379
|
|
147
|
+
|
|
148
|
+
PORT=3000
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/templates/core.ts
|
|
153
|
+
function mainTs(n) {
|
|
154
|
+
return `import 'reflect-metadata';
|
|
155
|
+
import { createExiumApp } from '@exium/core';
|
|
156
|
+
import { AppModule } from './app.module';
|
|
157
|
+
|
|
158
|
+
async function bootstrap() {
|
|
159
|
+
const app = await createExiumApp({
|
|
160
|
+
serviceName: '${n.serviceName}',
|
|
161
|
+
module: AppModule,
|
|
162
|
+
port: Number(process.env.PORT) || 3000,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await app.start();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
bootstrap();
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
function appModuleTs(n) {
|
|
172
|
+
return `import { Module } from '@nestjs/common';
|
|
173
|
+
import { ExiumModule } from '@exium/core';
|
|
174
|
+
import { ${n.EntityNames}Controller } from './api/${n.entityNames}.controller';
|
|
175
|
+
import { Create${n.EntityName}CommandHandler } from './application/commands/create-${n.entityName}/create-${n.entityName}.handler';
|
|
176
|
+
import { Get${n.EntityNames}QueryHandler } from './application/queries/get-${n.entityNames}/get-${n.entityNames}.handler';
|
|
177
|
+
import { ${n.EntityName}Repository } from './domain/${n.entityName}.repository';
|
|
178
|
+
import { ${n.EntityName}RepositoryAdapter } from './infrastructure/persistence/${n.entityName}.repository.adapter';
|
|
179
|
+
import { ${n.EntityName}Finder } from './application/ports/${n.entityName}.finder';
|
|
180
|
+
import { ${n.EntityName}FinderAdapter } from './infrastructure/persistence/${n.entityName}-finder.adapter';
|
|
181
|
+
import { ${n.EntityName}Mapper } from './infrastructure/persistence/${n.entityName}.mapper';
|
|
182
|
+
import { ${n.EntityName}OrmEntity } from './infrastructure/persistence/${n.entityName}.orm-entity';
|
|
183
|
+
|
|
184
|
+
@Module({
|
|
185
|
+
imports: [
|
|
186
|
+
ExiumModule.forRoot({
|
|
187
|
+
serviceName: '${n.serviceName}',
|
|
188
|
+
infrastructure: {
|
|
189
|
+
persistence: {
|
|
190
|
+
driver: 'postgres',
|
|
191
|
+
entities: [${n.EntityName}OrmEntity],
|
|
192
|
+
outbox: {
|
|
193
|
+
enabled: false,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
],
|
|
199
|
+
controllers: [${n.EntityNames}Controller],
|
|
200
|
+
providers: [
|
|
201
|
+
Create${n.EntityName}CommandHandler,
|
|
202
|
+
Get${n.EntityNames}QueryHandler,
|
|
203
|
+
${n.EntityName}Mapper,
|
|
204
|
+
{ provide: ${n.EntityName}Repository, useClass: ${n.EntityName}RepositoryAdapter },
|
|
205
|
+
{ provide: ${n.EntityName}Finder, useClass: ${n.EntityName}FinderAdapter },
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
export class AppModule {}
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/templates/domain.ts
|
|
213
|
+
function entityIdTs(n) {
|
|
214
|
+
return `import { Id } from '@exium/core';
|
|
215
|
+
|
|
216
|
+
export class ${n.EntityName}Id extends Id {
|
|
217
|
+
constructor(value: string) {
|
|
218
|
+
super(value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function aggregateTs(n) {
|
|
224
|
+
return `import { AuditableAggregateRoot } from '@exium/core';
|
|
225
|
+
import { ${n.EntityName}Id } from './${n.entityName}-id.value-object';
|
|
226
|
+
import { ${n.EntityName}CreatedEvent } from './${n.entityName}-created.event';
|
|
227
|
+
|
|
228
|
+
interface Rehydrate${n.EntityName}Props {
|
|
229
|
+
readonly id: ${n.EntityName}Id;
|
|
230
|
+
readonly name: string;
|
|
231
|
+
readonly createdAt: Date;
|
|
232
|
+
readonly updatedAt: Date;
|
|
233
|
+
readonly version: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export class ${n.EntityName} extends AuditableAggregateRoot<${n.EntityName}Id> {
|
|
237
|
+
private _name: string;
|
|
238
|
+
|
|
239
|
+
protected constructor(
|
|
240
|
+
id: ${n.EntityName}Id,
|
|
241
|
+
name: string,
|
|
242
|
+
createdAt = new Date(),
|
|
243
|
+
updatedAt = new Date(),
|
|
244
|
+
version = 1,
|
|
245
|
+
) {
|
|
246
|
+
super(id, createdAt, updatedAt, version);
|
|
247
|
+
this._name = name;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get name(): string {
|
|
251
|
+
return this._name;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static create(id: ${n.EntityName}Id, name: string): ${n.EntityName} {
|
|
255
|
+
const entity = new ${n.EntityName}(id, name);
|
|
256
|
+
entity.raise(new ${n.EntityName}CreatedEvent(id, name));
|
|
257
|
+
return entity;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static rehydrate(props: Rehydrate${n.EntityName}Props): ${n.EntityName} {
|
|
261
|
+
return new ${n.EntityName}(
|
|
262
|
+
props.id,
|
|
263
|
+
props.name,
|
|
264
|
+
props.createdAt,
|
|
265
|
+
props.updatedAt,
|
|
266
|
+
props.version,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
function domainEventTs(n) {
|
|
273
|
+
return `import { DomainEvent } from '@exium/core';
|
|
274
|
+
import { ${n.EntityName}Id } from './${n.entityName}-id.value-object';
|
|
275
|
+
|
|
276
|
+
export class ${n.EntityName}CreatedEvent extends DomainEvent {
|
|
277
|
+
readonly name = '${n.EntityName}Created';
|
|
278
|
+
|
|
279
|
+
constructor(
|
|
280
|
+
public readonly ${n.entityName}Id: ${n.EntityName}Id,
|
|
281
|
+
public readonly ${n.entityName}Name: string,
|
|
282
|
+
) {
|
|
283
|
+
super(${n.entityName}Id.value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
function repositoryTs(n) {
|
|
289
|
+
return `import { type Repository } from '@exium/core';
|
|
290
|
+
import { ${n.EntityName} } from './${n.entityName}.aggregate';
|
|
291
|
+
import { ${n.EntityName}Id } from './${n.entityName}-id.value-object';
|
|
292
|
+
|
|
293
|
+
export abstract class ${n.EntityName}Repository implements Repository<${n.EntityName}, ${n.EntityName}Id> {
|
|
294
|
+
abstract findById(id: ${n.EntityName}Id): Promise<${n.EntityName} | null>;
|
|
295
|
+
abstract findByIds(ids: readonly ${n.EntityName}Id[]): Promise<${n.EntityName}[]>;
|
|
296
|
+
abstract exists(id: ${n.EntityName}Id): Promise<boolean>;
|
|
297
|
+
abstract save(aggregate: ${n.EntityName}): Promise<void>;
|
|
298
|
+
abstract delete(id: ${n.EntityName}Id): Promise<void>;
|
|
299
|
+
abstract findByName(name: string): Promise<${n.EntityName} | null>;
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/templates/application.ts
|
|
305
|
+
function readModelTs(n) {
|
|
306
|
+
return `import { type ReadModel } from '@exium/core';
|
|
307
|
+
|
|
308
|
+
export class ${n.EntityName}ReadModel implements ReadModel {
|
|
309
|
+
constructor(
|
|
310
|
+
readonly id: string,
|
|
311
|
+
readonly name: string,
|
|
312
|
+
readonly createdAt: Date,
|
|
313
|
+
) {}
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
function finderPortTs(n) {
|
|
318
|
+
return `import { type Finder, type Page, type PageRequest } from '@exium/core';
|
|
319
|
+
import { ${n.EntityName}ReadModel } from '../dtos/${n.entityName}-read.model';
|
|
320
|
+
|
|
321
|
+
export abstract class ${n.EntityName}Finder implements Finder {
|
|
322
|
+
abstract findAll(pageRequest: PageRequest): Promise<Page<${n.EntityName}ReadModel>>;
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
function createCommandTs(n) {
|
|
327
|
+
return `import { Command, Transactional } from '@exium/core';
|
|
328
|
+
|
|
329
|
+
@Transactional()
|
|
330
|
+
export class Create${n.EntityName}Command extends Command<string> {
|
|
331
|
+
constructor(public readonly name: string) {
|
|
332
|
+
super();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
function createHandlerTs(n) {
|
|
338
|
+
return `import { Injectable } from '@nestjs/common';
|
|
339
|
+
import {
|
|
340
|
+
CommandHandler,
|
|
341
|
+
Failure,
|
|
342
|
+
HandleCommand,
|
|
343
|
+
Result,
|
|
344
|
+
type ResultType,
|
|
345
|
+
} from '@exium/core';
|
|
346
|
+
import { Create${n.EntityName}Command } from './create-${n.entityName}.command';
|
|
347
|
+
import { ${n.EntityName} } from '../../../domain/${n.entityName}.aggregate';
|
|
348
|
+
import { ${n.EntityName}Id } from '../../../domain/${n.entityName}-id.value-object';
|
|
349
|
+
import { ${n.EntityName}Repository } from '../../../domain/${n.entityName}.repository';
|
|
350
|
+
|
|
351
|
+
@Injectable()
|
|
352
|
+
@HandleCommand(Create${n.EntityName}Command)
|
|
353
|
+
export class Create${n.EntityName}CommandHandler extends CommandHandler<Create${n.EntityName}Command> {
|
|
354
|
+
constructor(private readonly ${n.entityName}Repository: ${n.EntityName}Repository) {
|
|
355
|
+
super();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async handle(command: Create${n.EntityName}Command): Promise<ResultType<string>> {
|
|
359
|
+
const existing = await this.${n.entityName}Repository.findByName(command.name);
|
|
360
|
+
|
|
361
|
+
if (existing) {
|
|
362
|
+
return Result.fail(
|
|
363
|
+
Failure.conflict(
|
|
364
|
+
'${n.EntityName.toUpperCase()}_ALREADY_EXISTS',
|
|
365
|
+
\`"\${command.name}" ad\u0131yla kay\u0131tl\u0131 bir ${n.entityName} zaten var.\`,
|
|
366
|
+
{ name: command.name },
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const id = ${n.EntityName}Id.generate();
|
|
372
|
+
const entity = ${n.EntityName}.create(id, command.name);
|
|
373
|
+
|
|
374
|
+
await this.${n.entityName}Repository.save(entity);
|
|
375
|
+
|
|
376
|
+
return Result.ok(id.value);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
function getQueryTs(n) {
|
|
382
|
+
return `import { Query, type Page, type PageRequest } from '@exium/core';
|
|
383
|
+
import { ${n.EntityName}ReadModel } from '../../dtos/${n.entityName}-read.model';
|
|
384
|
+
|
|
385
|
+
export class Get${n.EntityNames}Query extends Query<Page<${n.EntityName}ReadModel>> {
|
|
386
|
+
constructor(public readonly pageRequest: PageRequest) {
|
|
387
|
+
super();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
function getHandlerTs(n) {
|
|
393
|
+
return `import { Injectable } from '@nestjs/common';
|
|
394
|
+
import { HandleQuery, QueryHandler, Result, type Page, type ResultType } from '@exium/core';
|
|
395
|
+
import { Get${n.EntityNames}Query } from './get-${n.entityNames}.query';
|
|
396
|
+
import { ${n.EntityName}ReadModel } from '../../dtos/${n.entityName}-read.model';
|
|
397
|
+
import { ${n.EntityName}Finder } from '../../ports/${n.entityName}.finder';
|
|
398
|
+
|
|
399
|
+
@Injectable()
|
|
400
|
+
@HandleQuery(Get${n.EntityNames}Query)
|
|
401
|
+
export class Get${n.EntityNames}QueryHandler extends QueryHandler<Get${n.EntityNames}Query> {
|
|
402
|
+
constructor(private readonly ${n.entityName}Finder: ${n.EntityName}Finder) {
|
|
403
|
+
super();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async handle(query: Get${n.EntityNames}Query): Promise<ResultType<Page<${n.EntityName}ReadModel>>> {
|
|
407
|
+
const page = await this.${n.entityName}Finder.findAll(query.pageRequest);
|
|
408
|
+
|
|
409
|
+
return Result.ok(page);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/templates/infrastructure.ts
|
|
416
|
+
function ormEntityTs(n) {
|
|
417
|
+
return `import { defineEntity, p } from '@mikro-orm/core';
|
|
418
|
+
|
|
419
|
+
const ${n.EntityName}OrmEntitySchema = defineEntity({
|
|
420
|
+
name: '${n.EntityName}OrmEntity',
|
|
421
|
+
tableName: '${n.entityNames}',
|
|
422
|
+
properties: {
|
|
423
|
+
id: p.string().primary(),
|
|
424
|
+
name: p.string(),
|
|
425
|
+
createdAt: p.datetime(),
|
|
426
|
+
updatedAt: p.datetime().onUpdate(() => new Date()),
|
|
427
|
+
version: p.integer(),
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
export class ${n.EntityName}OrmEntity extends ${n.EntityName}OrmEntitySchema.class {}
|
|
432
|
+
|
|
433
|
+
${n.EntityName}OrmEntitySchema.setClass(${n.EntityName}OrmEntity);
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function mapperTs(n) {
|
|
437
|
+
return `import { Injectable } from '@nestjs/common';
|
|
438
|
+
import { type PersistenceMapper } from '@exium/core';
|
|
439
|
+
import { ${n.EntityName} } from '../../domain/${n.entityName}.aggregate';
|
|
440
|
+
import { ${n.EntityName}Id } from '../../domain/${n.entityName}-id.value-object';
|
|
441
|
+
import { ${n.EntityName}OrmEntity } from './${n.entityName}.orm-entity';
|
|
442
|
+
|
|
443
|
+
@Injectable()
|
|
444
|
+
export class ${n.EntityName}Mapper implements PersistenceMapper<${n.EntityName}, ${n.EntityName}Id, ${n.EntityName}OrmEntity, string> {
|
|
445
|
+
toDomain(persistence: ${n.EntityName}OrmEntity): ${n.EntityName} {
|
|
446
|
+
return ${n.EntityName}.rehydrate({
|
|
447
|
+
id: ${n.EntityName}Id.of(persistence.id),
|
|
448
|
+
name: persistence.name,
|
|
449
|
+
createdAt: persistence.createdAt,
|
|
450
|
+
updatedAt: persistence.updatedAt,
|
|
451
|
+
version: persistence.version,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
toPersistence(domain: ${n.EntityName}): ${n.EntityName}OrmEntity {
|
|
456
|
+
const entity = new ${n.EntityName}OrmEntity();
|
|
457
|
+
entity.id = domain.id.value;
|
|
458
|
+
entity.name = domain.name;
|
|
459
|
+
entity.createdAt = domain.createdAt;
|
|
460
|
+
entity.updatedAt = domain.updatedAt;
|
|
461
|
+
entity.version = domain.version;
|
|
462
|
+
return entity;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
toPersistenceId(id: ${n.EntityName}Id): string {
|
|
466
|
+
return id.value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getPersistenceId(domain: ${n.EntityName}): string {
|
|
470
|
+
return domain.id.value;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
function repositoryAdapterTs(n) {
|
|
476
|
+
return `import { Injectable } from '@nestjs/common';
|
|
477
|
+
import { EntityManager } from '@mikro-orm/core';
|
|
478
|
+
import { MikroOrmAggregateRepository, UnitOfWork } from '@exium/core';
|
|
479
|
+
import { ${n.EntityName} } from '../../domain/${n.entityName}.aggregate';
|
|
480
|
+
import { ${n.EntityName}Id } from '../../domain/${n.entityName}-id.value-object';
|
|
481
|
+
import { ${n.EntityName}Repository } from '../../domain/${n.entityName}.repository';
|
|
482
|
+
import { ${n.EntityName}OrmEntity } from './${n.entityName}.orm-entity';
|
|
483
|
+
import { ${n.EntityName}Mapper } from './${n.entityName}.mapper';
|
|
484
|
+
|
|
485
|
+
@Injectable()
|
|
486
|
+
export class ${n.EntityName}RepositoryAdapter
|
|
487
|
+
extends MikroOrmAggregateRepository<${n.EntityName}, ${n.EntityName}Id, ${n.EntityName}OrmEntity, string>
|
|
488
|
+
implements ${n.EntityName}Repository
|
|
489
|
+
{
|
|
490
|
+
constructor(
|
|
491
|
+
em: EntityManager,
|
|
492
|
+
private readonly ${n.entityName}Mapper: ${n.EntityName}Mapper,
|
|
493
|
+
uow: UnitOfWork,
|
|
494
|
+
) {
|
|
495
|
+
super(${n.EntityName}OrmEntity, em, ${n.entityName}Mapper, uow);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async findByName(name: string): Promise<${n.EntityName} | null> {
|
|
499
|
+
const raw = await this.em.findOne(${n.EntityName}OrmEntity, { name } as any);
|
|
500
|
+
if (!raw) return null;
|
|
501
|
+
return this.${n.entityName}Mapper.toDomain(raw);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
function finderAdapterTs(n) {
|
|
507
|
+
return `import { Injectable } from '@nestjs/common';
|
|
508
|
+
import { EntityManager } from '@mikro-orm/core';
|
|
509
|
+
import { Page, type PageRequest } from '@exium/core';
|
|
510
|
+
import { ${n.EntityName}Finder } from '../../application/ports/${n.entityName}.finder';
|
|
511
|
+
import { ${n.EntityName}ReadModel } from '../../application/dtos/${n.entityName}-read.model';
|
|
512
|
+
import { ${n.EntityName}OrmEntity } from './${n.entityName}.orm-entity';
|
|
513
|
+
|
|
514
|
+
@Injectable()
|
|
515
|
+
export class ${n.EntityName}FinderAdapter implements ${n.EntityName}Finder {
|
|
516
|
+
constructor(private readonly em: EntityManager) {}
|
|
517
|
+
|
|
518
|
+
async findAll(pageRequest: PageRequest): Promise<Page<${n.EntityName}ReadModel>> {
|
|
519
|
+
const [rows, totalItems] = await Promise.all([
|
|
520
|
+
this.em.find(${n.EntityName}OrmEntity, {}, {
|
|
521
|
+
limit: pageRequest.size,
|
|
522
|
+
offset: pageRequest.offset,
|
|
523
|
+
orderBy: { createdAt: 'desc' } as any,
|
|
524
|
+
}),
|
|
525
|
+
this.em.count(${n.EntityName}OrmEntity, {}),
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
const items = rows.map(
|
|
529
|
+
(row) => new ${n.EntityName}ReadModel(row.id, row.name, row.createdAt),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
return Page.of(items, pageRequest, totalItems);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/templates/api.ts
|
|
539
|
+
function createRequestTs(n) {
|
|
540
|
+
return `import { IsString, IsNotEmpty } from 'class-validator';
|
|
541
|
+
|
|
542
|
+
export class Create${n.EntityName}Request {
|
|
543
|
+
@IsString()
|
|
544
|
+
@IsNotEmpty()
|
|
545
|
+
name!: string;
|
|
546
|
+
}
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
function controllerTs(n) {
|
|
550
|
+
return `import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
|
551
|
+
import { CommandExecutor, ExternalApi, PageRequest, QueryExecutor } from '@exium/core';
|
|
552
|
+
import { Create${n.EntityName}Command } from '../application/commands/create-${n.entityName}/create-${n.entityName}.command';
|
|
553
|
+
import { Get${n.EntityNames}Query } from '../application/queries/get-${n.entityNames}/get-${n.entityNames}.query';
|
|
554
|
+
import { Create${n.EntityName}Request } from './dtos/create-${n.entityName}.request';
|
|
555
|
+
|
|
556
|
+
@Controller('${n.entityNames}')
|
|
557
|
+
@ExternalApi({
|
|
558
|
+
name: '${n.entityNames}',
|
|
559
|
+
owner: '${n.serviceName}',
|
|
560
|
+
tags: ['${n.entityNames}'],
|
|
561
|
+
})
|
|
562
|
+
export class ${n.EntityNames}Controller {
|
|
563
|
+
constructor(
|
|
564
|
+
private readonly commandExecutor: CommandExecutor,
|
|
565
|
+
private readonly queryExecutor: QueryExecutor,
|
|
566
|
+
) {}
|
|
567
|
+
|
|
568
|
+
@Post()
|
|
569
|
+
create(@Body() body: Create${n.EntityName}Request) {
|
|
570
|
+
return this.commandExecutor.execute(
|
|
571
|
+
new Create${n.EntityName}Command(body.name),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
@Get()
|
|
576
|
+
list(
|
|
577
|
+
@Query('page') page = '1',
|
|
578
|
+
@Query('size') size = '10',
|
|
579
|
+
) {
|
|
580
|
+
const pageRequest = PageRequest.of(Number(page), Number(size));
|
|
581
|
+
|
|
582
|
+
if (pageRequest.isFailure()) {
|
|
583
|
+
return pageRequest;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return this.queryExecutor.execute(
|
|
587
|
+
new Get${n.EntityNames}Query(pageRequest.value),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/commands/new.ts
|
|
595
|
+
function write(filePath, content) {
|
|
596
|
+
import_fs.default.mkdirSync(import_path.default.dirname(filePath), { recursive: true });
|
|
597
|
+
import_fs.default.writeFileSync(filePath, content, "utf-8");
|
|
598
|
+
}
|
|
599
|
+
async function runNew(serviceName, options) {
|
|
600
|
+
const entityInput = options.entity ?? deriveEntityFromService(serviceName);
|
|
601
|
+
const n = buildNames(serviceName, entityInput);
|
|
602
|
+
const root = import_path.default.resolve(process.cwd(), n.serviceName);
|
|
603
|
+
console.log();
|
|
604
|
+
console.log(import_chalk.default.bold(` Exium \u2014 yeni mikroservis olu\u015Fturuluyor`));
|
|
605
|
+
console.log(import_chalk.default.dim(` Servis : ${import_chalk.default.white(n.serviceName)}`));
|
|
606
|
+
console.log(import_chalk.default.dim(` Entity : ${import_chalk.default.white(n.EntityName)}`));
|
|
607
|
+
console.log(import_chalk.default.dim(` Konum : ${import_chalk.default.white(root)}`));
|
|
608
|
+
console.log();
|
|
609
|
+
if (import_fs.default.existsSync(root)) {
|
|
610
|
+
console.error(import_chalk.default.red(` Hata: "${root}" dizini zaten var.`));
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
const spinner = (0, import_ora.default)({ text: "Dosyalar olu\u015Fturuluyor\u2026", prefixText: " " }).start();
|
|
614
|
+
try {
|
|
615
|
+
write(`${root}/package.json`, packageJson(n));
|
|
616
|
+
write(`${root}/tsconfig.json`, tsConfig());
|
|
617
|
+
write(`${root}/.env.example`, envExample(n));
|
|
618
|
+
write(`${root}/.gitignore`, "node_modules\ndist\n.env\n");
|
|
619
|
+
write(`${root}/src/main.ts`, mainTs(n));
|
|
620
|
+
write(`${root}/src/app.module.ts`, appModuleTs(n));
|
|
621
|
+
write(`${root}/src/domain/${n.entityName}-id.value-object.ts`, entityIdTs(n));
|
|
622
|
+
write(`${root}/src/domain/${n.entityName}.aggregate.ts`, aggregateTs(n));
|
|
623
|
+
write(`${root}/src/domain/${n.entityName}-created.event.ts`, domainEventTs(n));
|
|
624
|
+
write(`${root}/src/domain/${n.entityName}.repository.ts`, repositoryTs(n));
|
|
625
|
+
write(`${root}/src/application/dtos/${n.entityName}-read.model.ts`, readModelTs(n));
|
|
626
|
+
write(`${root}/src/application/ports/${n.entityName}.finder.ts`, finderPortTs(n));
|
|
627
|
+
write(
|
|
628
|
+
`${root}/src/application/commands/create-${n.entityName}/create-${n.entityName}.command.ts`,
|
|
629
|
+
createCommandTs(n)
|
|
630
|
+
);
|
|
631
|
+
write(
|
|
632
|
+
`${root}/src/application/commands/create-${n.entityName}/create-${n.entityName}.handler.ts`,
|
|
633
|
+
createHandlerTs(n)
|
|
634
|
+
);
|
|
635
|
+
write(
|
|
636
|
+
`${root}/src/application/queries/get-${n.entityNames}/get-${n.entityNames}.query.ts`,
|
|
637
|
+
getQueryTs(n)
|
|
638
|
+
);
|
|
639
|
+
write(
|
|
640
|
+
`${root}/src/application/queries/get-${n.entityNames}/get-${n.entityNames}.handler.ts`,
|
|
641
|
+
getHandlerTs(n)
|
|
642
|
+
);
|
|
643
|
+
write(`${root}/src/infrastructure/persistence/${n.entityName}.orm-entity.ts`, ormEntityTs(n));
|
|
644
|
+
write(`${root}/src/infrastructure/persistence/${n.entityName}.mapper.ts`, mapperTs(n));
|
|
645
|
+
write(
|
|
646
|
+
`${root}/src/infrastructure/persistence/${n.entityName}.repository.adapter.ts`,
|
|
647
|
+
repositoryAdapterTs(n)
|
|
648
|
+
);
|
|
649
|
+
write(
|
|
650
|
+
`${root}/src/infrastructure/persistence/${n.entityName}-finder.adapter.ts`,
|
|
651
|
+
finderAdapterTs(n)
|
|
652
|
+
);
|
|
653
|
+
write(`${root}/src/api/dtos/create-${n.entityName}.request.ts`, createRequestTs(n));
|
|
654
|
+
write(`${root}/src/api/${n.entityNames}.controller.ts`, controllerTs(n));
|
|
655
|
+
spinner.succeed(import_chalk.default.green("Dosyalar olu\u015Fturuldu."));
|
|
656
|
+
} catch (err) {
|
|
657
|
+
spinner.fail("Dosya olu\u015Fturma ba\u015Far\u0131s\u0131z.");
|
|
658
|
+
throw err;
|
|
659
|
+
}
|
|
660
|
+
if (!options.skipInstall) {
|
|
661
|
+
const installSpinner = (0, import_ora.default)({ text: "Ba\u011F\u0131ml\u0131l\u0131klar y\xFCkleniyor\u2026", prefixText: " " }).start();
|
|
662
|
+
try {
|
|
663
|
+
(0, import_child_process.execSync)("npm install", { cwd: root, stdio: "ignore" });
|
|
664
|
+
installSpinner.succeed(import_chalk.default.green("Ba\u011F\u0131ml\u0131l\u0131klar y\xFCklendi."));
|
|
665
|
+
} catch {
|
|
666
|
+
installSpinner.warn(import_chalk.default.yellow("npm install ba\u015Far\u0131s\u0131z \u2014 manuel olarak \xE7al\u0131\u015Ft\u0131r."));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
console.log();
|
|
670
|
+
console.log(import_chalk.default.bold(" Haz\u0131r!"));
|
|
671
|
+
console.log();
|
|
672
|
+
console.log(` ${import_chalk.default.dim("$")} cd ${n.serviceName}`);
|
|
673
|
+
if (options.skipInstall) {
|
|
674
|
+
console.log(` ${import_chalk.default.dim("$")} npm install`);
|
|
675
|
+
}
|
|
676
|
+
console.log(` ${import_chalk.default.dim("$")} cp .env.example .env`);
|
|
677
|
+
console.log(` ${import_chalk.default.dim("$")} npm run start:dev`);
|
|
678
|
+
console.log();
|
|
679
|
+
console.log(
|
|
680
|
+
import_chalk.default.dim(` Sonraki ad\u0131m: src/domain/${n.entityName}.aggregate.ts dosyas\u0131n\u0131`) + import_chalk.default.dim(` domain'e g\xF6re d\xFCzenle.`)
|
|
681
|
+
);
|
|
682
|
+
console.log();
|
|
683
|
+
}
|
|
684
|
+
function deriveEntityFromService(serviceName) {
|
|
685
|
+
const parts = toKebab(serviceName).split("-");
|
|
686
|
+
return parts[0];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/index.ts
|
|
690
|
+
var program = new import_commander.Command();
|
|
691
|
+
program.name("exium").description("Exium Framework CLI").version("1.0.0");
|
|
692
|
+
program.command("new <service-name>").description("Yeni bir Exium mikroservisi olu\u015Fturur").option("-e, --entity <entity-name>", "\u0130lk entity ad\u0131 (belirtilmezse servis ad\u0131ndan t\xFCretilir)").option("--skip-install", "Ba\u011F\u0131ml\u0131l\u0131k kurulumunu atla").action((serviceName, options) => {
|
|
693
|
+
runNew(serviceName, options).catch((err) => {
|
|
694
|
+
console.error(err);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exium/cli",
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "CLI for scaffolding Exium microservices",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"exium": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.17.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"build:watch": "tsup --watch",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"dev": "ts-node src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"exium",
|
|
27
|
+
"nestjs",
|
|
28
|
+
"ddd",
|
|
29
|
+
"microservice",
|
|
30
|
+
"cli",
|
|
31
|
+
"scaffold"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/yusuf218921/exium-framework.git",
|
|
36
|
+
"directory": "cli"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/yusuf218921/exium-framework/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/yusuf218921/exium-framework/tree/main/cli#readme",
|
|
42
|
+
"author": "Yusuf Sönmez <yusufsonmez951@gmail.com>",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"chalk": "^4.1.2",
|
|
46
|
+
"commander": "^12.1.0",
|
|
47
|
+
"ora": "^5.4.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^25.6.0",
|
|
51
|
+
"ts-node": "^10.9.2",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"typescript": "^6.0.3"
|
|
54
|
+
}
|
|
55
|
+
}
|