@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.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/bin/cli.js +134 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # About
8
8
 
9
- NZMT is a toolkit for building structured Next.js full-stack applications.
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 move fast without losing predictability
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, `UserRAMStore` and `UserPrismaStore` implementations
61
- - `UserService` proxying all `UserStore` methods
62
- - `UserController` proxying all `UserService` methods
63
- - API `route handlers` can be used from client
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alevnyacow/nzmt",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Next Zod Modules Toolkit",
5
5
  "repository": {
6
6
  "type": "git",