@alevnyacow/nzmt 0.14.0 → 0.15.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 +8 -7
- package/bin/cli.js +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# About
|
|
8
8
|
|
|
9
|
-
NZMT is a
|
|
9
|
+
NZMT is a combination of a convenient library for building application modules validated with Zod schemas and a powerful scaffolding tool.
|
|
10
10
|
|
|
11
11
|
It combines dependency injection, Zod validation and a DDD-inspired architecture,
|
|
12
12
|
while removing most of the boilerplate through code generation out of the box.
|
|
@@ -19,9 +19,9 @@ Batteries included!
|
|
|
19
19
|
- You’re tired of rewriting CRUD, data layer logic, DTOs, and validation
|
|
20
20
|
- You want to follow best practices without overengineering or repetitive boilerplate
|
|
21
21
|
- You want your application to be runtime-safe
|
|
22
|
-
- You want to
|
|
22
|
+
- You don't want to waste time on another framework, you want to keep using plain Next.js, but with better structure and speed
|
|
23
23
|
- You want a backend that can evolve into a full-stack solution
|
|
24
|
-
- You dig cool cartoonish fonts in logos
|
|
24
|
+
- You dig cool cartoonish fonts in logos and you don't see it this much nowadays
|
|
25
25
|
|
|
26
26
|
You focus on business logic; NZMT handles the infrastructure.
|
|
27
27
|
|
|
@@ -57,10 +57,11 @@ npx nzmt crud-api user
|
|
|
57
57
|
This will generate:
|
|
58
58
|
|
|
59
59
|
- `User` entity
|
|
60
|
-
- `UserStore` contract
|
|
61
|
-
- `UserService`
|
|
62
|
-
- `UserController`
|
|
63
|
-
-
|
|
60
|
+
- `UserStore` contract with `RAM` and `Prisma` implementations
|
|
61
|
+
- `UserService` with all business methods
|
|
62
|
+
- `UserController` with ready-to-use API endpoints
|
|
63
|
+
- Fully typed `UserAPI` contract (endpoints + DTOs) for client usage
|
|
64
|
+
- API `route handlers` inside your `app` folder
|
|
64
65
|
|
|
65
66
|
All code is editable - you stay in full control.
|
|
66
67
|
|
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
|
}
|