@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.
Files changed (3) hide show
  1. package/README.md +21 -19
  2. package/bin/cli.js +134 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,35 +6,32 @@
6
6
 
7
7
  # About
8
8
 
9
- NZMT is a toolkit for building structured Next.js full-stack applications.
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
- It combines dependency injection, Zod validation and a DDD-inspired architecture,
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
- - You want to focus on domain logic without full DDD complexity
19
- - You’re tired of rewriting CRUD, data layer logic, DTOs, and validation
20
- - You want to follow best practices without overengineering or repetitive boilerplate
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
- You focus on business logic; NZMT handles the infrastructure.
21
+ # Quick start (Prisma + client-side queries)
27
22
 
28
- # Quick start with Prisma
23
+ Assuming you have a Next.js project with a generated Prisma client, `User` Prisma model and configured `@tanstack/react-query`:
29
24
 
30
- Assuming you have a Next.js project with a generated Prisma client and a `User` Prisma model:
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
- 4. Now you can scaffold everything you need for `User` entity CRUD API in one CLI command:
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
- 5. **Describe entity properties and validation rules using Zod** for the `User` entity in the scaffolded file `/shared/entities/user/user.entity.ts`.
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
- 6. **Implement Prisma mappers** in `/server/stores/user/user.store.prisma.ts`.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alevnyacow/nzmt",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "Next Zod Modules Toolkit",
5
5
  "repository": {
6
6
  "type": "git",