@alevnyacow/nzmt 0.6.1 → 0.7.1
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/README.md +116 -145
- package/bin/cli.js +188 -131
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,171 +1,142 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Next Zod Modules Toolkit
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/@alevnyacow%2Fnzmt)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# About
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
- `methods` function
|
|
10
|
-
- `Metadata` type
|
|
11
|
-
- `DTOs` type
|
|
12
|
-
- `Methods` type
|
|
13
|
-
- `Config` type
|
|
14
|
-
- [Controller](#controller)
|
|
15
|
-
- `endpoints` function
|
|
16
|
-
- `DefaultErrorCodes` enum
|
|
17
|
-
- `Guard` type
|
|
18
|
-
- `OnErrorHandler` type
|
|
19
|
-
- `SharedConfig` type
|
|
20
|
-
- `Metadata` type
|
|
21
|
-
- `Contract` type
|
|
22
|
-
- [Store](#store)
|
|
23
|
-
- `methods` function
|
|
24
|
-
- `InRAM` class generator
|
|
25
|
-
- `Types` type
|
|
26
|
-
- `Metadata` type
|
|
27
|
-
- `Contract` type
|
|
8
|
+
NZMT is an opinionated toolkit with scaffolding for building structured Next.js applications with DI, Zod validation, and DDD-inspired architecture — without boilerplate overhead.
|
|
28
9
|
|
|
29
|
-
|
|
10
|
+
- 🧩 **End-to-end contracts and implementations** — generated or manually authored — across backend and client–server integration layers.
|
|
11
|
+
- 🔐 **Runtime safety** across server layers with Zod out of the box.
|
|
12
|
+
- ⚡ **Dependency Injection** powered by Inversify with no setup required.
|
|
13
|
+
- 🚀 Comes with **scaffolding system** to generate and organize application structure via CLI.
|
|
30
14
|
|
|
31
|
-
|
|
15
|
+
# Quick start with scaffolding
|
|
32
16
|
|
|
33
|
-
|
|
17
|
+
```bash
|
|
18
|
+
# scaffolder initialization with Prisma (must be done once)
|
|
19
|
+
npx nzmt init prismaClientPath:@prisma/client
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Method schemas description
|
|
44
|
-
schemas: {
|
|
45
|
-
// Method `basicOperations`
|
|
46
|
-
basicOperation: {
|
|
47
|
-
// `basicOperations` payload (Zod schema)
|
|
48
|
-
payload: z.object({
|
|
49
|
-
lhs: z.number(),
|
|
50
|
-
rhs: z.number(),
|
|
51
|
-
operation: z.enum(['+', '-', '/', '*'])
|
|
52
|
-
}),
|
|
53
|
-
// `basicOperations` response (Zod schema)
|
|
54
|
-
response: z.object({
|
|
55
|
-
result: z.number()
|
|
56
|
-
})
|
|
57
|
-
},
|
|
58
|
-
// ...another methods
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Use `... satisfies Module.Metadata` instead of
|
|
62
|
-
* `const mathServiceMetadata: Module.Metadata = ...`
|
|
63
|
-
* for correct TS autocompletion in your IDE when
|
|
64
|
-
* using `Module.methods`.
|
|
65
|
-
*/
|
|
66
|
-
} satisfies Module.Metadata
|
|
67
|
-
|
|
68
|
-
const mathServiceMethods = Module.methods(mathServiceMetadata)
|
|
21
|
+
# product entity with title and price fields
|
|
22
|
+
npx nzmt entity product f:title-string,price-int.positive
|
|
23
|
+
# product store (with Prisma implementation, RAM implementation and DI)
|
|
24
|
+
npx nzmt store product
|
|
25
|
+
# product service with injected user store and product store
|
|
26
|
+
npx nzmt service product i:ProductStore
|
|
27
|
+
# shop controller with injected shop service and logger
|
|
28
|
+
npx nzmt controller shop i:Logger,ProductService
|
|
69
29
|
```
|
|
70
30
|
|
|
71
|
-
|
|
31
|
+
# Design principles
|
|
72
32
|
|
|
73
|
-
|
|
74
|
-
export class MathService {
|
|
75
|
-
// created generator
|
|
76
|
-
private methods = mathServiceMethods
|
|
77
|
-
|
|
78
|
-
public basicOperation = this.methods(
|
|
79
|
-
// Method name, working TS intellisense
|
|
80
|
-
'basicOperation',
|
|
81
|
-
// handler logic
|
|
82
|
-
async (
|
|
83
|
-
// payload, also with TS intellisense
|
|
84
|
-
{ lhs, operation, rhs },
|
|
85
|
-
// errors generator
|
|
86
|
-
{ methodError }
|
|
87
|
-
) => {
|
|
88
|
-
switch(operation) {
|
|
89
|
-
case '*': {
|
|
90
|
-
// all types are infered from schemas, so
|
|
91
|
-
// TS intellisense also works with return types
|
|
92
|
-
return { result: lhs * rhs }
|
|
93
|
-
}
|
|
94
|
-
case '+': {
|
|
95
|
-
return { result: lhs + rhs }
|
|
96
|
-
}
|
|
97
|
-
case '-': {
|
|
98
|
-
return { result: lhs - rhs }
|
|
99
|
-
}
|
|
100
|
-
case '/': {
|
|
101
|
-
if (rhs === 0) {
|
|
102
|
-
/**
|
|
103
|
-
* You can create error with just code. All
|
|
104
|
-
* metadata like method name, zod module name
|
|
105
|
-
* or payload will present in this error object.
|
|
106
|
-
*/
|
|
107
|
-
throw methodError('DIVIDED_BY_ZERO')
|
|
108
|
-
}
|
|
109
|
-
return { result: lhs / rhs }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
```
|
|
33
|
+
## Core idea
|
|
116
34
|
|
|
117
|
-
|
|
35
|
+
DDD is powerful, but it truly shines in large-scale systems with large teams. In practice, developers often face a trade-off:
|
|
36
|
+
either adopt heavy architectural concepts or build with little to no structure at all. **Mature engineering is about trade-offs. A good tool should help you make them intentionally.**
|
|
118
37
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
38
|
+
That’s what this toolkit is designed for. It brings the benefits of DDD without unnecessary complexity that can slow down early development — and adds scaffolding to move even faster. **Automate what’s routine. Stay flexible for what’s complex.**
|
|
39
|
+
|
|
40
|
+
## Server-side layer
|
|
41
|
+
|
|
42
|
+
Server-side logic is structured into four core modules: *Stores*, *Services*, *Controllers*, and *Providers*.
|
|
43
|
+
|
|
44
|
+
- **Stores** encapsulate Data Layer logic with no business rules.
|
|
45
|
+
- **Services** define business logic and use-case flows.
|
|
46
|
+
- **Controllers** handle API requests and delegate work to services.
|
|
47
|
+
- **Providers** manage integrations with external systems (e.g. email, third-party APIs).
|
|
48
|
+
|
|
49
|
+
## Shared layer
|
|
50
|
+
|
|
51
|
+
There are also two building blocks shared across server and client: Entities and Value Objects.
|
|
52
|
+
|
|
53
|
+
- **Entities** represent domain objects used throughout the application. They contain no Data Layer logic and are not responsible for business use-case logic, but may include pure domain logic, contracts, and invariants (e.g. User, Product).
|
|
54
|
+
- **Value Objects** define reusable, strongly-typed invariants for meaningful concepts such as Pagination or Identifier.
|
|
55
|
+
|
|
56
|
+
# Scaffolding
|
|
57
|
+
|
|
58
|
+
## Setup
|
|
59
|
+
|
|
60
|
+
1. Install required dependencies:
|
|
61
|
+
```bash
|
|
62
|
+
npm i inversify zod
|
|
63
|
+
```
|
|
64
|
+
These are not peer dependencies, so you can use NZMT with only required features.
|
|
65
|
+
|
|
66
|
+
2. Initialize scaffolding:
|
|
67
|
+
```
|
|
68
|
+
npx nzmt init prismaClientPath:@prisma/client
|
|
128
69
|
```
|
|
70
|
+
This creates `nzmt.config.json`, sets up DI and testing, and adds base providers. `prismaClientPath:...` parameter is optional and enables Prisma scaffolding.
|
|
129
71
|
|
|
130
|
-
|
|
72
|
+
## Naming conventions
|
|
131
73
|
|
|
132
|
-
|
|
74
|
+
- The entity name is expected to be **in `kebab-case`** (e.g. `awesome-user`, `product`).
|
|
75
|
+
- The entity name is expected to be **in singular form** (e.g. `product` instead of `products`).
|
|
133
76
|
|
|
134
|
-
|
|
77
|
+
## Shared layer modules
|
|
135
78
|
|
|
136
|
-
|
|
79
|
+
### Entities
|
|
137
80
|
|
|
138
|
-
Example:
|
|
81
|
+
Example: scaffolding a `User` entity with two fields (name and age).
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx nzmt entity user f:name-string,age-int.positive
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can define entity fields using the `f:` flag. The format is `name-type`, where `type` maps to Zod (e.g. `int.positive` → `z.int().positive()`). This is optional — `npx nzmt entity user` will scaffold a `User` entity without additional fields.
|
|
88
|
+
|
|
89
|
+
Entity scaffolder generates a dedicated folder with a barrel file and an entity implementation. Generated code is fully editable — you stay in control.
|
|
90
|
+
|
|
91
|
+
The generated `user.entity.ts` looks like this:
|
|
139
92
|
|
|
140
93
|
```ts
|
|
141
|
-
import
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
94
|
+
import z from 'zod'
|
|
95
|
+
import { ValueObjects } from '@alevnyacow/nzmt'
|
|
96
|
+
|
|
97
|
+
export type UserModel = z.infer<typeof User.schema>
|
|
98
|
+
|
|
99
|
+
export class User {
|
|
100
|
+
static schema = z.object({
|
|
101
|
+
id: ValueObjects.Identifier.schema,
|
|
102
|
+
name: z.string(),
|
|
103
|
+
age: z.int().positive(),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
private constructor(private readonly data: UserModel) {}
|
|
107
|
+
|
|
108
|
+
static create = (data: UserModel) => {
|
|
109
|
+
const parsedModel = User.schema.parse(data)
|
|
110
|
+
return new User(parsedModel)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get model(): UserModel {
|
|
114
|
+
return this.data
|
|
154
115
|
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
type TestServiceDTOs = Module.DTOs<typeof testMetadata>
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* TestServiceDTOs:
|
|
161
|
-
* {
|
|
162
|
-
* testMethodPayload: { stringField: string }
|
|
163
|
-
* testMethodResponse: { result: boolean }
|
|
164
|
-
* }
|
|
165
|
-
*
|
|
166
|
-
*/
|
|
116
|
+
}
|
|
167
117
|
```
|
|
168
118
|
|
|
169
|
-
|
|
119
|
+
`User` entity, `User.schema` zod schema and `UserModel` type can be used wherever they are needed.
|
|
120
|
+
|
|
121
|
+
# Package API
|
|
170
122
|
|
|
171
|
-
|
|
123
|
+
- Module
|
|
124
|
+
- `methods` function
|
|
125
|
+
- `Metadata` type
|
|
126
|
+
- `DTOs` type
|
|
127
|
+
- `Methods` type
|
|
128
|
+
- `Config` type
|
|
129
|
+
- Controller
|
|
130
|
+
- `endpoints` function
|
|
131
|
+
- `DefaultErrorCodes` enum
|
|
132
|
+
- `Guard` type
|
|
133
|
+
- `OnErrorHandler` type
|
|
134
|
+
- `SharedConfig` type
|
|
135
|
+
- `Metadata` type
|
|
136
|
+
- `Contract` type
|
|
137
|
+
- Store
|
|
138
|
+
- `methods` function
|
|
139
|
+
- `InRAM` class generator
|
|
140
|
+
- `Types` type
|
|
141
|
+
- `Metadata` type
|
|
142
|
+
- `Contract` type
|
package/bin/cli.js
CHANGED
|
@@ -18,14 +18,14 @@ function insertAfterLineInFile(filePath, targetLine, newLine) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function insertBeforeLineInFile(filePath, targetLine, newLine) {
|
|
21
|
+
function insertBeforeLineInFile(filePath, targetLine, newLine, before = true) {
|
|
22
22
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
23
23
|
const lines = content.split('\n');
|
|
24
24
|
|
|
25
25
|
const index = lines.findIndex(line => line.includes(targetLine));
|
|
26
26
|
|
|
27
27
|
if (index !== -1) {
|
|
28
|
-
lines.splice(index, 0, newLine);
|
|
28
|
+
lines.splice(before ? index - 1 : index, 0, newLine);
|
|
29
29
|
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -155,7 +155,7 @@ function initDI() {
|
|
|
155
155
|
"\t// Providers",
|
|
156
156
|
"\t// Services",
|
|
157
157
|
"\t// Controllers",
|
|
158
|
-
"\t//
|
|
158
|
+
"\t// Infrastructure",
|
|
159
159
|
"} satisfies DIEntries",
|
|
160
160
|
"",
|
|
161
161
|
"export type DITokens = keyof typeof diEntries",
|
|
@@ -312,20 +312,65 @@ function initPrisma() {
|
|
|
312
312
|
insertBeforeLineInFile(
|
|
313
313
|
diEntriesPath,
|
|
314
314
|
'type DIEntries =',
|
|
315
|
-
`import { prismaClient } from '${config?.paths?.infrastructure.replace('./src', '@')}/prisma'
|
|
315
|
+
`import { prismaClient } from '${config?.paths?.infrastructure.replace('./src', '@')}/prisma'`
|
|
316
316
|
)
|
|
317
317
|
|
|
318
318
|
insertAfterLineInFile(
|
|
319
319
|
diEntriesPath,
|
|
320
|
-
'//
|
|
320
|
+
'// Infrastructure',
|
|
321
321
|
`\tPrismaClient: { constantValue: prismaClient },`,
|
|
322
322
|
)
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
function initLogger() {
|
|
326
|
+
const config = loadConfig()
|
|
327
|
+
const loggerFolder = path.resolve(process.cwd(), config?.paths?.infrastructure, 'logger')
|
|
328
|
+
fs.mkdirSync(loggerFolder, { recursive: true })
|
|
329
|
+
|
|
330
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'logger.ts'), [
|
|
331
|
+
`export abstract class Logger {`,
|
|
332
|
+
`\tabstract error: (payload: Record<string, unknown>) => Promise<void>`,
|
|
333
|
+
`}`
|
|
334
|
+
].join('\n'))
|
|
335
|
+
|
|
336
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'logger.console.ts'), [
|
|
337
|
+
`import { injectable } from 'inversify'`,
|
|
338
|
+
`import { Logger } from './logger'`,
|
|
339
|
+
'',
|
|
340
|
+
'@injectable()',
|
|
341
|
+
`export class ConsoleLogger extends Logger {`,
|
|
342
|
+
`\terror: Logger['error'] = async (payload) => console.error(payload)`,
|
|
343
|
+
`}`
|
|
344
|
+
].join('\n'))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'index.ts'), [
|
|
348
|
+
`export * from './logger'`,
|
|
349
|
+
`export * from './logger.console'`
|
|
350
|
+
].join('\n'))
|
|
351
|
+
|
|
352
|
+
// Update DI
|
|
353
|
+
|
|
354
|
+
const diEntriesPath = path.resolve(process.cwd(), config?.paths?.di, 'entries.di.ts')
|
|
355
|
+
|
|
356
|
+
insertBeforeLineInFile(
|
|
357
|
+
diEntriesPath,
|
|
358
|
+
'type DIEntries =',
|
|
359
|
+
`import { ConsoleLogger } from '${config?.paths?.infrastructure.replace('./src', '@')}/logger'`
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
insertAfterLineInFile(
|
|
363
|
+
diEntriesPath,
|
|
364
|
+
'// Infrastructure',
|
|
365
|
+
`\tLogger: ConsoleLogger,`,
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
325
369
|
if (command.toLowerCase() === 'init' || command === 'i') {
|
|
326
370
|
createDefaultConfig()
|
|
327
371
|
initDI()
|
|
328
372
|
initPrisma()
|
|
373
|
+
initLogger()
|
|
329
374
|
|
|
330
375
|
process.exit(0)
|
|
331
376
|
}
|
|
@@ -335,7 +380,7 @@ function generateStores(lowerCase, upperCase, withEntityPreset) {
|
|
|
335
380
|
|
|
336
381
|
fs.mkdirSync(folder, { recursive: true })
|
|
337
382
|
|
|
338
|
-
const withEntity = withEntityPreset || (options ?? []).includes('
|
|
383
|
+
const withEntity = withEntityPreset || !(options ?? []).includes('dont-import-entity')
|
|
339
384
|
|
|
340
385
|
// Contract
|
|
341
386
|
|
|
@@ -509,7 +554,7 @@ function generateStores(lowerCase, upperCase, withEntityPreset) {
|
|
|
509
554
|
insertBeforeLineInFile(
|
|
510
555
|
diEntriesPath,
|
|
511
556
|
'type DIEntries =',
|
|
512
|
-
prismaPath ? `import { ${upperCase}PrismaStore, ${upperCase}RAMStore } from '${config?.paths?.stores.replace('./src', '@')}/${entityName}'
|
|
557
|
+
prismaPath ? `import { ${upperCase}PrismaStore, ${upperCase}RAMStore } from '${config?.paths?.stores.replace('./src', '@')}/${entityName}'` : `import { ${upperCase}RAMStore } from '${config?.paths?.stores.replace('./src', '@')}/${entityName}'`
|
|
513
558
|
)
|
|
514
559
|
|
|
515
560
|
insertAfterLineInFile(
|
|
@@ -632,7 +677,7 @@ function generateProvider(lowerCase, upperCase) {
|
|
|
632
677
|
`type Methods = Module.Methods<typeof ${lowerCase}ProviderMetadata>;`,
|
|
633
678
|
``,
|
|
634
679
|
`export abstract class ${upperCase}Provider {`,
|
|
635
|
-
`\
|
|
680
|
+
`\tprotected methods = Module.methods(${lowerCase}ProviderMetadata)`,
|
|
636
681
|
`}`
|
|
637
682
|
].join('\n'))
|
|
638
683
|
|
|
@@ -650,7 +695,7 @@ function generateProvider(lowerCase, upperCase) {
|
|
|
650
695
|
`import { ${upperCase}Provider } from './${entityName}.provider'`,
|
|
651
696
|
'',
|
|
652
697
|
`export class ${upperCase}${providerType}Provider extends ${upperCase}Provider {`,
|
|
653
|
-
`\
|
|
698
|
+
`\tprotected method = zodModuleMethodFactory(mailProviderMetadata);`,
|
|
654
699
|
`}`
|
|
655
700
|
].join('\n'))
|
|
656
701
|
|
|
@@ -666,7 +711,7 @@ function generateProvider(lowerCase, upperCase) {
|
|
|
666
711
|
insertBeforeLineInFile(
|
|
667
712
|
diEntriesPath,
|
|
668
713
|
'type DIEntries =',
|
|
669
|
-
`import { ${upperCase}MockProvider, ${upperCase}${providerType}Provider } from '${config?.paths?.providers.replace('./src', '@')}/${entityName}}'
|
|
714
|
+
`import { ${upperCase}MockProvider, ${upperCase}${providerType}Provider } from '${config?.paths?.providers.replace('./src', '@')}/${entityName}}'`
|
|
670
715
|
)
|
|
671
716
|
|
|
672
717
|
insertAfterLineInFile(
|
|
@@ -682,131 +727,65 @@ if (command.toLowerCase() === 'provider' || command === 'p') {
|
|
|
682
727
|
process.exit(0)
|
|
683
728
|
}
|
|
684
729
|
|
|
730
|
+
function toKebabFromPascal(str) {
|
|
731
|
+
return str
|
|
732
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
733
|
+
.toLowerCase()
|
|
734
|
+
}
|
|
685
735
|
|
|
686
|
-
function generateService(lowerCase, upperCase
|
|
736
|
+
function generateService(lowerCase, upperCase) {
|
|
687
737
|
const folder = config?.paths?.services ? path.resolve(process.cwd(), config?.paths?.services, entityName) : path.resolve(process.cwd(), entityName);
|
|
688
738
|
|
|
739
|
+
const injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
|
|
740
|
+
|
|
741
|
+
const importInjections = injections.map((i) => {
|
|
742
|
+
if (i.endsWith('Service') || i.endsWith('Controller')) {
|
|
743
|
+
throw 'Only stores and infrastructure can be injected in services'
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (i.endsWith('Store')) {
|
|
747
|
+
return `import { ${i} } from '${config?.paths?.stores?.replace('./src', '@')}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return `import { ${i} } from '${config?.paths?.infrastructure?.replace('./src', '@')}/${toKebabFromPascal(i)}'`
|
|
751
|
+
})
|
|
752
|
+
|
|
689
753
|
fs.mkdirSync(folder, { recursive: true })
|
|
690
754
|
|
|
691
755
|
// Metadata
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
`\t\t\t}),`,
|
|
705
|
-
`\t\t\tresponse: z.object({`,
|
|
706
|
-
`\t\t\t\titem: ${lowerCase}StoreMetadata.models.details.nullable()`,
|
|
707
|
-
`\t\t\t})`,
|
|
708
|
-
`\t\t},`,
|
|
709
|
-
`\t\tgetList: {`,
|
|
710
|
-
`\t\t\tpayload: z.object({`,
|
|
711
|
-
`\t\t\t\tfilter: ${lowerCase}StoreMetadata.searchPayload.list,`,
|
|
712
|
-
`\t\t\t\tpagination: ValueObjects.Pagination.schema.optional()`,
|
|
713
|
-
`\t\t\t}),`,
|
|
714
|
-
`\t\t\tresponse: z.object({`,
|
|
715
|
-
`\t\t\t\titems: z.array(${lowerCase}StoreMetadata.models.list)`,
|
|
716
|
-
`\t\t\t})`,
|
|
717
|
-
`\t\t},`,
|
|
718
|
-
`\t\tupdateOne: {`,
|
|
719
|
-
`\t\t\tpayload: z.object({`,
|
|
720
|
-
`\t\t\t\tfilter: ${lowerCase}StoreMetadata.searchPayload.specific,`,
|
|
721
|
-
`\t\t\t\tpayload: ${lowerCase}StoreMetadata.actionsPayload.update`,
|
|
722
|
-
`\t\t\t}),`,
|
|
723
|
-
`\t\t\tresponse: z.object({})`,
|
|
724
|
-
`\t\t},`,
|
|
725
|
-
`\t\tcreate: {`,
|
|
726
|
-
`\t\t\tpayload: z.object({`,
|
|
727
|
-
`\t\t\t\tpayload: ${lowerCase}StoreMetadata.actionsPayload.create`,
|
|
728
|
-
`\t\t\t}),`,
|
|
729
|
-
`\t\t\tresponse: z.object({`,
|
|
730
|
-
`\t\t\t\tid: ValueObjects.Identifier.schema`,
|
|
731
|
-
`\t\t\t}),`,
|
|
732
|
-
`\t\t},`,
|
|
733
|
-
`\t\tdeleteOne: {`,
|
|
734
|
-
`\t\t\tpayload: z.object({`,
|
|
735
|
-
`\t\t\t\tfilter: ${lowerCase}StoreMetadata.searchPayload.specific`,
|
|
736
|
-
`\t\t\t}),`,
|
|
737
|
-
`\t\t\tresponse: z.object({})`,
|
|
738
|
-
`\t\t},`,
|
|
739
|
-
"\t}",
|
|
740
|
-
"} satisfies Module.Metadata",
|
|
741
|
-
"",
|
|
742
|
-
`export type ${upperCase}ServiceDTOs = Module.DTOs<typeof ${lowerCase}ServiceMetadata>`
|
|
743
|
-
].filter(x => typeof x === 'string').join('\n'))
|
|
744
|
-
} else {
|
|
745
|
-
fs.writeFileSync(path.resolve(folder, `${entityName}.service.metadata.ts`), [
|
|
746
|
-
"import type { Module } from '@alevnyacow/nzmt'",
|
|
747
|
-
"",
|
|
748
|
-
`export const ${lowerCase}ServiceMetadata = {`,
|
|
749
|
-
`\tname: '${upperCase}Service',`,
|
|
750
|
-
"\tschemas: {}",
|
|
751
|
-
"} satisfies Module.Metadata",
|
|
752
|
-
"",
|
|
753
|
-
`export type ${upperCase}ServiceDTOs = Module.DTOs<typeof ${lowerCase}ServiceMetadata>`
|
|
754
|
-
].filter(x => typeof x === 'string').join('\n'))
|
|
755
|
-
}
|
|
756
|
+
|
|
757
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.service.metadata.ts`), [
|
|
758
|
+
"import type { Module } from '@alevnyacow/nzmt'",
|
|
759
|
+
"",
|
|
760
|
+
`export const ${lowerCase}ServiceMetadata = {`,
|
|
761
|
+
`\tname: '${upperCase}Service',`,
|
|
762
|
+
"\tschemas: {}",
|
|
763
|
+
"} satisfies Module.Metadata",
|
|
764
|
+
"",
|
|
765
|
+
`export type ${upperCase}ServiceDTOs = Module.DTOs<typeof ${lowerCase}ServiceMetadata>`
|
|
766
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
767
|
+
|
|
756
768
|
|
|
757
769
|
// Service body
|
|
758
770
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
`\tgetSpecific = this.method('getSpecific', async (x) => {`,
|
|
778
|
-
`\t\tconst item = await ${lowerCase}Store.details(x);`,
|
|
779
|
-
`\t\treturn { item };`,
|
|
780
|
-
`\t})`,
|
|
781
|
-
`\t`,
|
|
782
|
-
`\tgetList = this.method('getList', async (x) => {`,
|
|
783
|
-
`\t\tconst items = await ${lowerCase}Store.list(x);`,
|
|
784
|
-
`\t\treturn { items };`,
|
|
785
|
-
`\t})`,
|
|
786
|
-
`\t`,
|
|
787
|
-
`\tupdateOne = this.method('updateOne', async (x) => {`,
|
|
788
|
-
`\t\tawait ${lowerCase}Store.updateOne(x);`,
|
|
789
|
-
`\t\treturn {};`,
|
|
790
|
-
`\t})`,
|
|
791
|
-
`\t`,
|
|
792
|
-
`\tdeleteOne = this.method('deleteOne', async (x) => {`,
|
|
793
|
-
`\t\tawait ${lowerCase}Store.deleteOne(x);`,
|
|
794
|
-
`\t\treturn {};`,
|
|
795
|
-
`\t})`,
|
|
796
|
-
"}"
|
|
797
|
-
].filter(x => typeof x === 'string').join('\n'))
|
|
798
|
-
} else {
|
|
799
|
-
fs.writeFileSync(path.resolve(folder, `${entityName}.service.ts`), [
|
|
800
|
-
"import { injectable } from 'inversify'",
|
|
801
|
-
`import { ${lowerCase}ServiceMetadata } from './${entityName}.service.metadata'`,
|
|
802
|
-
"import { Module } from '@alevnyacow/nzmt'",
|
|
803
|
-
"",
|
|
804
|
-
"@injectable()",
|
|
805
|
-
`export class ${upperCase}Service {`,
|
|
806
|
-
`\tprivate methods = Module.methods(${lowerCase}ServiceMetadata)`,
|
|
807
|
-
"}"
|
|
808
|
-
].filter(x => typeof x === 'string').join('\n'))
|
|
809
|
-
}
|
|
771
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.service.ts`), [
|
|
772
|
+
"import { injectable } from 'inversify'",
|
|
773
|
+
injections.length ? `import { DITokens } from '${config?.paths?.di?.replace('./src', '@')}'` : undefined,
|
|
774
|
+
`import { ${lowerCase}ServiceMetadata } from './${entityName}.service.metadata'`,
|
|
775
|
+
"import { Module } from '@alevnyacow/nzmt'",
|
|
776
|
+
...importInjections,
|
|
777
|
+
"",
|
|
778
|
+
"@injectable()",
|
|
779
|
+
`export class ${upperCase}Service {`,
|
|
780
|
+
`\tprivate methods = Module.methods(${lowerCase}ServiceMetadata)`,
|
|
781
|
+
``,
|
|
782
|
+
`\tconstructor(`,
|
|
783
|
+
...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
|
|
784
|
+
`\t) {}`,
|
|
785
|
+
``,
|
|
786
|
+
"}"
|
|
787
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
788
|
+
|
|
810
789
|
|
|
811
790
|
// Barrel
|
|
812
791
|
|
|
@@ -822,7 +801,7 @@ function generateService(lowerCase, upperCase, withCrud) {
|
|
|
822
801
|
insertBeforeLineInFile(
|
|
823
802
|
diEntriesPath,
|
|
824
803
|
'type DIEntries =',
|
|
825
|
-
`import { ${upperCase}Service } from '${config?.paths?.services.replace('./src', '@')}/${entityName}
|
|
804
|
+
`import { ${upperCase}Service } from '${config?.paths?.services.replace('./src', '@')}/${entityName}'`
|
|
826
805
|
)
|
|
827
806
|
|
|
828
807
|
insertAfterLineInFile(
|
|
@@ -838,10 +817,88 @@ if (command.toLowerCase() === 'service' || command === 'S') {
|
|
|
838
817
|
process.exit(0)
|
|
839
818
|
}
|
|
840
819
|
|
|
841
|
-
|
|
820
|
+
function generateController(upperCase, lowerCase) {
|
|
821
|
+
const folder = config?.paths?.controllers ? path.resolve(process.cwd(), config?.paths?.controllers, entityName) : path.resolve(process.cwd(), entityName);
|
|
822
|
+
|
|
823
|
+
const injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
|
|
824
|
+
|
|
825
|
+
const importInjections = injections.map((i) => {
|
|
826
|
+
if (i.endsWith('Controller')) {
|
|
827
|
+
throw 'Only stores, services and infrastructure can be injected in controllers'
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (i.endsWith('Store')) {
|
|
831
|
+
return `import { ${i} } from '${config?.paths?.stores?.replace('./src', '@')}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (i.endsWith('Service')) {
|
|
835
|
+
return `import { ${i} } from '${config?.paths?.services?.replace('./src', '@')}/${toKebabFromPascal(i).slice(0, -'-service'.length)}'`
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return `import { ${i} } from '${config?.paths?.infrastructure?.replace('./src', '@')}/${toKebabFromPascal(i)}'`
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
842
|
+
|
|
843
|
+
// Metadata
|
|
844
|
+
|
|
845
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.controller.metadata.ts`), [
|
|
846
|
+
`import { Controller } from '@alevnyacow/nzmt'`,
|
|
847
|
+
``,
|
|
848
|
+
`export const ${lowerCase}ControllerMetadata = {`,
|
|
849
|
+
`\tname: '${upperCase}Controller',`,
|
|
850
|
+
`\tschemas: {}`,
|
|
851
|
+
`} satisfies Controller.Metadata`,
|
|
852
|
+
``,
|
|
853
|
+
`export type ${upperCase}API = Controller.Contract<typeof ${lowerCase}ControllerMetadata>`
|
|
854
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
855
|
+
|
|
856
|
+
// Body
|
|
857
|
+
|
|
858
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.controller.ts`), [
|
|
859
|
+
`import { Controller } from '@alevnyacow/nzmt'`,
|
|
860
|
+
`import { injectable } from 'inversify'`,
|
|
861
|
+
`import { DITokens } from '${config?.paths?.di?.replace('./src', '@')}'`,
|
|
862
|
+
`import { ${lowerCase}ControllerMetadata } from './${entityName}.controller.metadata'`,
|
|
863
|
+
...importInjections,
|
|
864
|
+
``,
|
|
865
|
+
`@injectable()`,
|
|
866
|
+
`export class ${upperCase}Controller {`,
|
|
867
|
+
`\tconstructor(`,
|
|
868
|
+
...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
|
|
869
|
+
`\t) {}`,
|
|
870
|
+
``,
|
|
871
|
+
`\tprivate readonly endpoints = Controller.endpoints(${lowerCase}ControllerMetadata)`,
|
|
872
|
+
``,
|
|
873
|
+
`}`
|
|
874
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
875
|
+
|
|
876
|
+
// Barrel
|
|
877
|
+
|
|
878
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
879
|
+
`export * from './${entityName}.controller'`,
|
|
880
|
+
`export * from './${entityName}.controller.metadata'`
|
|
881
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
882
|
+
|
|
883
|
+
// Update DI
|
|
884
|
+
|
|
885
|
+
const diEntriesPath = path.resolve(process.cwd(), config?.paths?.di, 'entries.di.ts')
|
|
886
|
+
|
|
887
|
+
insertBeforeLineInFile(
|
|
888
|
+
diEntriesPath,
|
|
889
|
+
'type DIEntries =',
|
|
890
|
+
`import { ${upperCase}Controller } from '${config?.paths?.controllers.replace('./src', '@')}/${entityName}'`
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
insertAfterLineInFile(
|
|
894
|
+
diEntriesPath,
|
|
895
|
+
'// Controllers',
|
|
896
|
+
`\t${upperCase}Controller,`,
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (command.toLowerCase() === 'controller' || command === 'c') {
|
|
842
901
|
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
843
|
-
|
|
844
|
-
generateStores(lowerCase, upperCase, true)
|
|
845
|
-
generateService(lowerCase, upperCase, true)
|
|
902
|
+
generateController(upperCase, lowerCase)
|
|
846
903
|
process.exit(0)
|
|
847
904
|
}
|