@alevnyacow/nzmt 0.14.1 → 0.15.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 +21 -19
- package/bin/cli.js +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,35 +6,32 @@
|
|
|
6
6
|
|
|
7
7
|
# About
|
|
8
8
|
|
|
9
|
-
NZMT is
|
|
9
|
+
**Not** a framework. Seriously, we have enough of those. NZMT is just the tools you always wanted in Next.js, plus a scaffolder that spins up server logic and client queries. Full-stack, the Next.js way!
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
while removing most of the boilerplate through code generation out of the box.
|
|
11
|
+
Think: dependency injection + Zod validation + DDD vibes... but without drowning in boilerplate. You write the fun stuff; NZMT handles the boring stuff.
|
|
13
12
|
|
|
14
|
-
Batteries included
|
|
13
|
+
Batteries included.
|
|
15
14
|
|
|
16
15
|
## Why NZMT?
|
|
17
16
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- You want your application to be runtime-safe
|
|
22
|
-
- You want to move fast without losing predictability
|
|
23
|
-
- You want a backend that can evolve into a full-stack solution
|
|
24
|
-
- You dig cool cartoonish fonts in logos
|
|
17
|
+
- Focus on your domain logic without drowning in full-blown DDD.
|
|
18
|
+
- Keep using plain Next.js, just faster and cleaner — no extra framework required.
|
|
19
|
+
- Watch entities, stores, services, controllers, handlers, and client-side queries appear automatically (and yes, it’s actually fun).
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
# Quick start (Prisma + client-side queries)
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
Assuming you have a Next.js project with a generated Prisma client, `User` Prisma model and configured `@tanstack/react-query`:
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
## Initialization
|
|
26
|
+
|
|
27
|
+
1. Install required dependencies and NZMT itself:
|
|
31
28
|
|
|
32
|
-
1. Install required peer dependencies and the toolkit itself.
|
|
33
29
|
```bash
|
|
34
30
|
npm install inversify zod reflect-metadata @alevnyacow/nzmt
|
|
35
31
|
```
|
|
36
32
|
|
|
37
33
|
2. Enable `Experimental decorators` and `Emit Decorator Metadata` options in your `tsconfig.json`.
|
|
34
|
+
|
|
38
35
|
```json
|
|
39
36
|
{
|
|
40
37
|
"compilerOptions": {
|
|
@@ -45,11 +42,15 @@ npm install inversify zod reflect-metadata @alevnyacow/nzmt
|
|
|
45
42
|
```
|
|
46
43
|
|
|
47
44
|
3. Initialize NZMT (must be done once). This will set up all required infrastructure and configuration for you:
|
|
45
|
+
|
|
48
46
|
```bash
|
|
49
47
|
npx nzmt init prismaClientPath:@/app/generated/prisma/client
|
|
50
48
|
```
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
## Scaffolding
|
|
51
|
+
|
|
52
|
+
Now you can scaffold everything you need for `User` entity CRUD API in one CLI command:
|
|
53
|
+
|
|
53
54
|
```bash
|
|
54
55
|
npx nzmt crud-api user
|
|
55
56
|
```
|
|
@@ -62,12 +63,13 @@ This will generate:
|
|
|
62
63
|
- `UserController` with ready-to-use API endpoints
|
|
63
64
|
- Fully typed `UserAPI` contract (endpoints + DTOs) for client usage
|
|
64
65
|
- API `route handlers` inside your `app` folder
|
|
66
|
+
- `React queries`. Fully typed and ready to be used in your client-side code!
|
|
65
67
|
|
|
66
|
-
All code is editable - you stay in full control
|
|
68
|
+
**All code is editable - you stay in full control!**
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
- **Describe entity properties and validation rules using Zod** for the `User` entity in the scaffolded file `/shared/entities/user/user.entity.ts`.
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
- **Implement Prisma mappers** in `/server/stores/user/user.store.prisma.ts`.
|
|
71
73
|
All methods and contracts are already scaffolded; you only need to describe the mappers themselves. RAM store implementation works out of the box.
|
|
72
74
|
|
|
73
75
|
# Design principles
|
package/bin/cli.js
CHANGED
|
@@ -111,6 +111,7 @@ function createDefaultConfig() {
|
|
|
111
111
|
entities: '/shared/entities',
|
|
112
112
|
valueObjects: '/shared/value-objects',
|
|
113
113
|
queries: '/client/shared/queries',
|
|
114
|
+
clientUtils: '/client/shared/utils'
|
|
114
115
|
},
|
|
115
116
|
store: {
|
|
116
117
|
prisma: {
|
|
@@ -129,6 +130,7 @@ function createDefaultConfig() {
|
|
|
129
130
|
entities: '/shared/entities',
|
|
130
131
|
valueObjects: '/shared/value-objects',
|
|
131
132
|
queries: '/client/shared/queries',
|
|
133
|
+
clientUtils: '/client/shared/utils'
|
|
132
134
|
}
|
|
133
135
|
}, null, '\t'))
|
|
134
136
|
}
|
|
@@ -289,6 +291,27 @@ function initDI() {
|
|
|
289
291
|
].join('\n'))
|
|
290
292
|
};
|
|
291
293
|
|
|
294
|
+
function initClientUtils() {
|
|
295
|
+
const config = loadConfig()
|
|
296
|
+
const root = findProjectRoot()
|
|
297
|
+
const folder = path.resolve(root, `${config.coreFolder}${config.paths.clientUtils}`)
|
|
298
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
299
|
+
|
|
300
|
+
fs.writeFileSync(path.resolve(folder, 'api-request.ts'), [
|
|
301
|
+
"export const apiRequest = (url: string, method: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT') =>",
|
|
302
|
+
"\tasync (payload: { body?: Object; query?: Object }) => {",
|
|
303
|
+
"\t\tconst query = payload?.query ? '?' + new URLSearchParams(Object.entries(payload.query).filter(([, v]) => v != null && v !== '')) : ''",
|
|
304
|
+
"\t\tconst res = await fetch(url + query, { method, headers: { 'Content-Type': 'application/json' }, body: payload?.body && JSON.stringify(payload.body) })",
|
|
305
|
+
"\t\tif (!res.ok) throw await res.json()",
|
|
306
|
+
"\t\treturn res.json()",
|
|
307
|
+
"\t}"
|
|
308
|
+
].join('\n'))
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), [
|
|
311
|
+
`export * from './api-request'`
|
|
312
|
+
].join('\n'))
|
|
313
|
+
}
|
|
314
|
+
|
|
292
315
|
function initPrisma() {
|
|
293
316
|
const config = loadConfig()
|
|
294
317
|
const prismaClientPath = config?.store?.prisma?.clientPath
|
|
@@ -377,6 +400,7 @@ function initLogger() {
|
|
|
377
400
|
if (command.toLowerCase() === 'init' || command === 'i') {
|
|
378
401
|
createDefaultConfig()
|
|
379
402
|
initDI()
|
|
403
|
+
initClientUtils()
|
|
380
404
|
initPrisma()
|
|
381
405
|
initLogger()
|
|
382
406
|
|
|
@@ -1030,6 +1054,108 @@ function generateAPIRoutes(lowerCase, upperCase) {
|
|
|
1030
1054
|
}
|
|
1031
1055
|
}
|
|
1032
1056
|
|
|
1057
|
+
function generateQueries(lowerCase, upperCase) {
|
|
1058
|
+
const projectRoot = findProjectRoot()
|
|
1059
|
+
|
|
1060
|
+
const fileText = fs.readFileSync(
|
|
1061
|
+
path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, entityName, `${entityName}.controller.ts`),
|
|
1062
|
+
'utf-8'
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
|
|
1066
|
+
const methods = Array.from(fileText.matchAll(regex), m => m[1])
|
|
1067
|
+
|
|
1068
|
+
const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
|
|
1069
|
+
|
|
1070
|
+
const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
|
|
1071
|
+
const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
|
|
1072
|
+
if (!acc[cur.path]) {
|
|
1073
|
+
acc[cur.path] = []
|
|
1074
|
+
}
|
|
1075
|
+
acc[cur.path].push(cur.method)
|
|
1076
|
+
return acc
|
|
1077
|
+
}, {})
|
|
1078
|
+
|
|
1079
|
+
const controllerQueriesPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${entityName}-controller`)
|
|
1080
|
+
|
|
1081
|
+
fs.mkdirSync(controllerQueriesPath, { recursive: true })
|
|
1082
|
+
|
|
1083
|
+
for (const rootMethod of rootMethods) {
|
|
1084
|
+
if (!rootMethod) {
|
|
1085
|
+
continue
|
|
1086
|
+
}
|
|
1087
|
+
const fileName = path.resolve(controllerQueriesPath, `${rootMethod}.ts` )
|
|
1088
|
+
const alreadyExists = fs.existsSync(fileName)
|
|
1089
|
+
if (alreadyExists) {
|
|
1090
|
+
continue
|
|
1091
|
+
}
|
|
1092
|
+
fs.writeFileSync(fileName, [
|
|
1093
|
+
`import { ${rootMethod === 'GET' ? 'useQuery' : 'useMutation'} } from '@tanstack/react-query'`,
|
|
1094
|
+
`import type { ${upperCase}API } from '@${config.paths.controllers}/${entityName}'`,
|
|
1095
|
+
`import { apiRequest } from '@${config.paths.clientUtils}'`,
|
|
1096
|
+
'',
|
|
1097
|
+
`type Method = ${upperCase}API['endpoints']['${rootMethod}']`,
|
|
1098
|
+
``,
|
|
1099
|
+
`const endpoint = '/api/${entityName}-controller'`,
|
|
1100
|
+
``,
|
|
1101
|
+
rootMethod === 'GET'
|
|
1102
|
+
? [
|
|
1103
|
+
`export const use${upperCase}Controller_${rootMethod} = (payload: Method['payload']) => {`,
|
|
1104
|
+
`\treturn useQuery<Method['response'], Method['error']>({`,
|
|
1105
|
+
`\t\tqueryKey: [endpoint, payload],`,
|
|
1106
|
+
`\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
|
|
1107
|
+
`\t})`,
|
|
1108
|
+
`}`
|
|
1109
|
+
].join('\n')
|
|
1110
|
+
: [
|
|
1111
|
+
`export const use${upperCase}Controller_${rootMethod} = () => {`,
|
|
1112
|
+
`\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
|
|
1113
|
+
`\t\tmutationFn: apiRequest(endpoint, '${rootMethod}')`,
|
|
1114
|
+
`\t})`,
|
|
1115
|
+
`}`
|
|
1116
|
+
].join('\n')
|
|
1117
|
+
].join('\n'))
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
for (const [currentPath, methods] of Object.entries(nestedMethods)) {
|
|
1121
|
+
for (const method of methods) {
|
|
1122
|
+
const fullMethodName = `${currentPath.replaceAll('/', '_')}_${method}`
|
|
1123
|
+
const fileName = path.resolve(controllerQueriesPath, `${fullMethodName}.ts`)
|
|
1124
|
+
const alreadyExists = fs.existsSync(fileName)
|
|
1125
|
+
if (alreadyExists) {
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
fs.writeFileSync(fileName, [
|
|
1129
|
+
`import { ${method === 'GET' ? 'useQuery' : 'useMutation'} } from '@tanstack/react-query'`,
|
|
1130
|
+
`import type { ${upperCase}API } from '@${config.paths.controllers}/${entityName}'`,
|
|
1131
|
+
`import { apiRequest } from '@${config.paths.clientUtils}'`,
|
|
1132
|
+
'',
|
|
1133
|
+
`type Method = ${upperCase}API['endpoints']['${fullMethodName}']`,
|
|
1134
|
+
``,
|
|
1135
|
+
`const endpoint = '/api/${entityName}-controller/${currentPath}'`,
|
|
1136
|
+
``,
|
|
1137
|
+
method === 'GET'
|
|
1138
|
+
? [
|
|
1139
|
+
`export const use${upperCase}Controller_${fullMethodName} = (payload: Method['payload']) => {`,
|
|
1140
|
+
`\treturn useQuery<Method['response'], Method['error']>({`,
|
|
1141
|
+
`\t\tqueryKey: [endpoint, payload],`,
|
|
1142
|
+
`\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
|
|
1143
|
+
`\t})`,
|
|
1144
|
+
`}`
|
|
1145
|
+
].join('\n')
|
|
1146
|
+
: [
|
|
1147
|
+
`export const use${upperCase}Controller_${fullMethodName} = () => {`,
|
|
1148
|
+
`\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
|
|
1149
|
+
`\t\tmutationFn: apiRequest(endpoint, '${method}')`,
|
|
1150
|
+
`\t})`,
|
|
1151
|
+
`}`
|
|
1152
|
+
].join('\n')
|
|
1153
|
+
].join('\n'))
|
|
1154
|
+
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1033
1159
|
|
|
1034
1160
|
if (command === 'api-routes') {
|
|
1035
1161
|
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
@@ -1037,6 +1163,13 @@ if (command === 'api-routes') {
|
|
|
1037
1163
|
generateAPIRoutes(lowerCase, upperCase)
|
|
1038
1164
|
}
|
|
1039
1165
|
|
|
1166
|
+
if (command === 'queries') {
|
|
1167
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1168
|
+
|
|
1169
|
+
generateQueries(lowerCase, upperCase)
|
|
1170
|
+
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1040
1173
|
if (command.toLowerCase() === 'controller' || command === 'c') {
|
|
1041
1174
|
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1042
1175
|
generateController(upperCase, lowerCase)
|
|
@@ -1065,5 +1198,6 @@ if (command.toLowerCase() === 'crud-api') {
|
|
|
1065
1198
|
generateService(lowerCase, upperCase, upperCase + 'Store')
|
|
1066
1199
|
generateController(upperCase, lowerCase, upperCase + 'Service')
|
|
1067
1200
|
generateAPIRoutes(lowerCase, upperCase)
|
|
1201
|
+
generateQueries(lowerCase, upperCase)
|
|
1068
1202
|
process.exit(0)
|
|
1069
1203
|
}
|