@alevnyacow/nzmt 0.13.1 → 0.14.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/README.md +5 -75
- package/bin/cli.js +58 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,6 @@ npm install inversify zod reflect-metadata @alevnyacow/nzmt
|
|
|
36
36
|
|
|
37
37
|
2. Enable `Experimental decorators` and `Emit Decorator Metadata` options in your `tsconfig.json`.
|
|
38
38
|
```json
|
|
39
|
-
// tsconfig.json
|
|
40
39
|
{
|
|
41
40
|
"compilerOptions": {
|
|
42
41
|
"experimentalDecorators": true,
|
|
@@ -45,7 +44,7 @@ npm install inversify zod reflect-metadata @alevnyacow/nzmt
|
|
|
45
44
|
}
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
3. Initialize NZMT. This will set up all required infrastructure and configuration for you:
|
|
47
|
+
3. Initialize NZMT (must be done once). This will set up all required infrastructure and configuration for you:
|
|
49
48
|
```bash
|
|
50
49
|
npx nzmt init prismaClientPath:@/app/generated/prisma/client
|
|
51
50
|
```
|
|
@@ -58,29 +57,17 @@ npx nzmt crud-api user
|
|
|
58
57
|
This will generate:
|
|
59
58
|
|
|
60
59
|
- `User` entity
|
|
61
|
-
- `UserStore` contract, `UserRAMStore` and `UserPrismaStore`
|
|
60
|
+
- `UserStore` contract, `UserRAMStore` and `UserPrismaStore` implementations
|
|
62
61
|
- `UserService` proxying all `UserStore` methods
|
|
63
|
-
- `UserController` proxying all `UserService` methods
|
|
62
|
+
- `UserController` proxying all `UserService` methods
|
|
63
|
+
- API `route handlers` can be used from client
|
|
64
64
|
|
|
65
65
|
All code is editable - you stay in full control.
|
|
66
66
|
|
|
67
67
|
5. **Describe entity properties and validation rules using Zod** for the `User` entity in the scaffolded file `/shared/entities/user/user.entity.ts`.
|
|
68
68
|
|
|
69
69
|
6. **Implement Prisma mappers** in `/server/stores/user/user.store.prisma.ts`.
|
|
70
|
-
All methods and contracts are already scaffolded; you only need to describe the mappers themselves.
|
|
71
|
-
|
|
72
|
-
7. Use generated controller in `app/api/user/route.ts` file via DI.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import type { UserController } from "@/server/controllers/user"
|
|
76
|
-
import { fromDI } from "@/server/di"
|
|
77
|
-
|
|
78
|
-
// Get a fully typed controller instance from the DI container.
|
|
79
|
-
// Key is fully typed too, of course.
|
|
80
|
-
const controller = fromDI<UserController>('UserController')
|
|
81
|
-
// Use controller method as a route method.
|
|
82
|
-
export const GET = controller.GET
|
|
83
|
-
```
|
|
70
|
+
All methods and contracts are already scaffolded; you only need to describe the mappers themselves. RAM store implementation works out of the box.
|
|
84
71
|
|
|
85
72
|
# Design principles
|
|
86
73
|
|
|
@@ -107,63 +94,6 @@ There are also two building blocks shared across server and client: Entities and
|
|
|
107
94
|
- **Entities** represent domain objects used throughout the application. They don’t include data access or business flow logic, but may contain pure domain logic, contracts and invariants (e.g. User, Product).
|
|
108
95
|
- **Value Objects** define reusable, strongly-typed invariants for meaningful concepts such as Pagination or Identifier.
|
|
109
96
|
|
|
110
|
-
# Scaffolding
|
|
111
|
-
|
|
112
|
-
## Setup
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
npx nzmt init prismaClientPath:@/app/generated/prisma/client
|
|
116
|
-
```
|
|
117
|
-
This creates `nzmt.config.json`, sets up DI and testing, and adds base providers. `prismaClientPath:...` parameter is optional and enables Prisma scaffolding.
|
|
118
|
-
|
|
119
|
-
## Naming conventions
|
|
120
|
-
|
|
121
|
-
- The entity name is expected to be **in `kebab-case`** (e.g. `awesome-user`, `product`).
|
|
122
|
-
- The entity name is expected to be **in singular form** (e.g. `product` instead of `products`).
|
|
123
|
-
|
|
124
|
-
## Shared layer modules
|
|
125
|
-
|
|
126
|
-
### Entities
|
|
127
|
-
|
|
128
|
-
Example: scaffolding a `User` entity with two fields (name and age).
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
npx nzmt entity user f:name-string,age-int.positive
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
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.
|
|
135
|
-
|
|
136
|
-
Entity scaffolder generates a dedicated folder with a barrel file and an entity implementation. Generated code is fully editable — you stay in control.
|
|
137
|
-
|
|
138
|
-
The generated `user.entity.ts` looks like this:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
import z from 'zod'
|
|
142
|
-
import { ValueObjects } from '@alevnyacow/nzmt'
|
|
143
|
-
|
|
144
|
-
export type UserModel = z.infer<typeof User.schema>
|
|
145
|
-
|
|
146
|
-
export class User {
|
|
147
|
-
static schema = z.object({
|
|
148
|
-
id: ValueObjects.Identifier.schema,
|
|
149
|
-
name: z.string(),
|
|
150
|
-
age: z.int().positive(),
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
private constructor(private readonly data: UserModel) {}
|
|
154
|
-
|
|
155
|
-
static create = (data: UserModel) => {
|
|
156
|
-
const parsedModel = User.schema.parse(data)
|
|
157
|
-
return new User(parsedModel)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get model(): UserModel {
|
|
161
|
-
return this.data
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
`User` entity, `User.schema` zod schema and `UserModel` type can be used wherever they are needed.
|
|
167
97
|
|
|
168
98
|
# Package API
|
|
169
99
|
|
package/bin/cli.js
CHANGED
|
@@ -980,6 +980,63 @@ function generateController(upperCase, lowerCase, crudService) {
|
|
|
980
980
|
)
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
+
function generateAPIRoutes(lowerCase, upperCase) {
|
|
984
|
+
const projectRoot = findProjectRoot()
|
|
985
|
+
const fileText = fs.readFileSync(
|
|
986
|
+
path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, entityName, `${entityName}.controller.ts`),
|
|
987
|
+
'utf-8'
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
|
|
991
|
+
const methods = Array.from(fileText.matchAll(regex), m => m[1])
|
|
992
|
+
|
|
993
|
+
const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
|
|
994
|
+
|
|
995
|
+
const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
|
|
996
|
+
const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
|
|
997
|
+
if (!acc[cur.path]) {
|
|
998
|
+
acc[cur.path] = []
|
|
999
|
+
}
|
|
1000
|
+
acc[cur.path].push(cur.method)
|
|
1001
|
+
return acc
|
|
1002
|
+
}, {})
|
|
1003
|
+
|
|
1004
|
+
const controllerHandlersRootPath = path.resolve(projectRoot, config.coreFolder, 'app', 'api', `${entityName}-controller`)
|
|
1005
|
+
|
|
1006
|
+
fs.mkdirSync(controllerHandlersRootPath, { recursive: true })
|
|
1007
|
+
|
|
1008
|
+
if (rootMethods.length) {
|
|
1009
|
+
fs.writeFileSync(path.resolve(controllerHandlersRootPath, 'route.ts'), [
|
|
1010
|
+
`import type { ${upperCase}Controller } from '@${config.paths.controllers}/${entityName}'`,
|
|
1011
|
+
`import { fromDI } from '@${config.paths.di}'`,
|
|
1012
|
+
'',
|
|
1013
|
+
`const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
|
|
1014
|
+
'',
|
|
1015
|
+
rootMethods.map(x => `export const ${x} = controller.${x}`).join('\n')
|
|
1016
|
+
].join('\n'))
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
for (const [currentPath, methods] of Object.entries(nestedMethods)) {
|
|
1020
|
+
const nestedFolder = path.resolve(controllerHandlersRootPath, currentPath)
|
|
1021
|
+
fs.mkdirSync(nestedFolder, { recursive: true })
|
|
1022
|
+
fs.writeFileSync(path.resolve(nestedFolder, 'route.ts'), [
|
|
1023
|
+
`import type { ${upperCase}Controller } from '@${config.paths.controllers}/${entityName}'`,
|
|
1024
|
+
`import { fromDI } from '@${config.paths.di}'`,
|
|
1025
|
+
'',
|
|
1026
|
+
`const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
|
|
1027
|
+
'',
|
|
1028
|
+
methods.map(x => `export const ${x} = controller.${currentPath.replaceAll('/', '_')}_${x}`).join('\n')
|
|
1029
|
+
].join('\n'))
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
if (command === 'api-routes') {
|
|
1035
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1036
|
+
|
|
1037
|
+
generateAPIRoutes(lowerCase, upperCase)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
983
1040
|
if (command.toLowerCase() === 'controller' || command === 'c') {
|
|
984
1041
|
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
985
1042
|
generateController(upperCase, lowerCase)
|
|
@@ -1007,5 +1064,6 @@ if (command.toLowerCase() === 'crud-api') {
|
|
|
1007
1064
|
generateStores(lowerCase, upperCase, true)
|
|
1008
1065
|
generateService(lowerCase, upperCase, upperCase + 'Store')
|
|
1009
1066
|
generateController(upperCase, lowerCase, upperCase + 'Service')
|
|
1067
|
+
generateAPIRoutes(lowerCase, upperCase)
|
|
1010
1068
|
process.exit(0)
|
|
1011
1069
|
}
|