@alevnyacow/nzmt 0.13.2 → 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 +2 -71
- package/bin/cli.js +58 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,8 @@ This will generate:
|
|
|
59
59
|
- `User` entity
|
|
60
60
|
- `UserStore` contract, `UserRAMStore` and `UserPrismaStore` implementations
|
|
61
61
|
- `UserService` proxying all `UserStore` methods
|
|
62
|
-
- `UserController` proxying all `UserService` methods
|
|
62
|
+
- `UserController` proxying all `UserService` methods
|
|
63
|
+
- API `route handlers` can be used from client
|
|
63
64
|
|
|
64
65
|
All code is editable - you stay in full control.
|
|
65
66
|
|
|
@@ -68,19 +69,6 @@ All code is editable - you stay in full control.
|
|
|
68
69
|
6. **Implement Prisma mappers** in `/server/stores/user/user.store.prisma.ts`.
|
|
69
70
|
All methods and contracts are already scaffolded; you only need to describe the mappers themselves. RAM store implementation works out of the box.
|
|
70
71
|
|
|
71
|
-
7. Use generated controller in `app/api/user/route.ts` file via DI.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import type { UserController } from "@/server/controllers/user"
|
|
75
|
-
import { fromDI } from "@/server/di"
|
|
76
|
-
|
|
77
|
-
// Get a fully typed controller instance from the DI container.
|
|
78
|
-
// Key is fully typed too, of course.
|
|
79
|
-
const controller = fromDI<UserController>('UserController')
|
|
80
|
-
// Use controller method as a route method.
|
|
81
|
-
export const GET = controller.GET
|
|
82
|
-
```
|
|
83
|
-
|
|
84
72
|
# Design principles
|
|
85
73
|
|
|
86
74
|
## Core idea
|
|
@@ -106,63 +94,6 @@ There are also two building blocks shared across server and client: Entities and
|
|
|
106
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).
|
|
107
95
|
- **Value Objects** define reusable, strongly-typed invariants for meaningful concepts such as Pagination or Identifier.
|
|
108
96
|
|
|
109
|
-
# Scaffolding
|
|
110
|
-
|
|
111
|
-
## Setup
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
npx nzmt init prismaClientPath:@/app/generated/prisma/client
|
|
115
|
-
```
|
|
116
|
-
This creates `nzmt.config.json`, sets up DI and testing, and adds base providers. `prismaClientPath:...` parameter is optional and enables Prisma scaffolding.
|
|
117
|
-
|
|
118
|
-
## Naming conventions
|
|
119
|
-
|
|
120
|
-
- The entity name is expected to be **in `kebab-case`** (e.g. `awesome-user`, `product`).
|
|
121
|
-
- The entity name is expected to be **in singular form** (e.g. `product` instead of `products`).
|
|
122
|
-
|
|
123
|
-
## Shared layer modules
|
|
124
|
-
|
|
125
|
-
### Entities
|
|
126
|
-
|
|
127
|
-
Example: scaffolding a `User` entity with two fields (name and age).
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
npx nzmt entity user f:name-string,age-int.positive
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
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.
|
|
134
|
-
|
|
135
|
-
Entity scaffolder generates a dedicated folder with a barrel file and an entity implementation. Generated code is fully editable — you stay in control.
|
|
136
|
-
|
|
137
|
-
The generated `user.entity.ts` looks like this:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import z from 'zod'
|
|
141
|
-
import { ValueObjects } from '@alevnyacow/nzmt'
|
|
142
|
-
|
|
143
|
-
export type UserModel = z.infer<typeof User.schema>
|
|
144
|
-
|
|
145
|
-
export class User {
|
|
146
|
-
static schema = z.object({
|
|
147
|
-
id: ValueObjects.Identifier.schema,
|
|
148
|
-
name: z.string(),
|
|
149
|
-
age: z.int().positive(),
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
private constructor(private readonly data: UserModel) {}
|
|
153
|
-
|
|
154
|
-
static create = (data: UserModel) => {
|
|
155
|
-
const parsedModel = User.schema.parse(data)
|
|
156
|
-
return new User(parsedModel)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
get model(): UserModel {
|
|
160
|
-
return this.data
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
`User` entity, `User.schema` zod schema and `UserModel` type can be used wherever they are needed.
|
|
166
97
|
|
|
167
98
|
# Package API
|
|
168
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
|
}
|