@currentjs/gen 0.5.6 → 0.5.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.7] - 2026-04-08
4
+
5
+ - HTTP errors in generated controllers
6
+
3
7
  ## [0.5.6] - 2026-04-08
4
8
 
5
9
  - database identifier types (numeric, uuid, nanoid)
@@ -11,6 +11,11 @@ export declare class ControllerGenerator {
11
11
  * Check if auth config includes owner permission
12
12
  */
13
13
  private hasOwnerAuth;
14
+ /**
15
+ * Determine which HTTP error classes from @currentjs/router are needed
16
+ * based on the auth configs of all endpoints in a controller.
17
+ */
18
+ private getNeededHttpErrorImports;
14
19
  /**
15
20
  * Generate pre-fetch authentication/authorization check code.
16
21
  * This runs before fetching the entity and validates authentication and role-based access.
@@ -78,6 +78,28 @@ class ControllerGenerator {
78
78
  const roles = this.normalizeAuth(auth);
79
79
  return roles.includes(constants_1.AUTH_ROLES.OWNER);
80
80
  }
81
+ /**
82
+ * Determine which HTTP error classes from @currentjs/router are needed
83
+ * based on the auth configs of all endpoints in a controller.
84
+ */
85
+ getNeededHttpErrorImports(auths) {
86
+ const needed = new Set();
87
+ for (const auth of auths) {
88
+ const roles = this.normalizeAuth(auth);
89
+ if (roles.length === 0 || (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.ALL))
90
+ continue;
91
+ needed.add('UnauthorizedError');
92
+ const roleChecks = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
93
+ if (roleChecks.length > 0) {
94
+ needed.add('ForbiddenError');
95
+ }
96
+ if (roles.includes(constants_1.AUTH_ROLES.OWNER)) {
97
+ needed.add('ForbiddenError');
98
+ needed.add('NotFoundError');
99
+ }
100
+ }
101
+ return Array.from(needed).sort();
102
+ }
81
103
  /**
82
104
  * Generate pre-fetch authentication/authorization check code.
83
105
  * This runs before fetching the entity and validates authentication and role-based access.
@@ -92,14 +114,14 @@ class ControllerGenerator {
92
114
  // If only 'authenticated' is specified
93
115
  if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.AUTHENTICATED) {
94
116
  return `if (!context.request.user) {
95
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
117
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
96
118
  }`;
97
119
  }
98
120
  // If only 'owner' is specified - just require authentication here
99
121
  // (owner check happens post-fetch)
100
122
  if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.OWNER) {
101
123
  return `if (!context.request.user) {
102
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
124
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
103
125
  }`;
104
126
  }
105
127
  // Filter out 'owner' and 'all' for role checks (owner is checked post-fetch)
@@ -111,16 +133,16 @@ class ControllerGenerator {
111
133
  if (roleChecks.length === 0) {
112
134
  // Only owner (and maybe authenticated) - just require auth
113
135
  return `if (!context.request.user) {
114
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
136
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
115
137
  }`;
116
138
  }
117
139
  if (roleChecks.length === 1 && !hasOwner) {
118
140
  // Single role check
119
141
  return `if (!context.request.user) {
120
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
142
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
121
143
  }
122
144
  if (context.request.user.role !== '${roleChecks[0]}') {
123
- throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
145
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
124
146
  }`;
125
147
  }
126
148
  // Multiple roles OR owner - use OR logic
@@ -129,22 +151,22 @@ class ControllerGenerator {
129
151
  if (hasOwner) {
130
152
  // With owner: require auth, role check will be combined with owner check post-fetch
131
153
  return `if (!context.request.user) {
132
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
154
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
133
155
  }`;
134
156
  }
135
157
  // Multiple roles without owner - check if user has ANY of the roles
136
158
  const roleConditions = roleChecks.map(r => `context.request.user.role === '${r}'`).join(' || ');
137
159
  return `if (!context.request.user) {
138
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
160
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
139
161
  }
140
162
  if (!(${roleConditions})) {
141
- throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
163
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
142
164
  }`;
143
165
  }
144
166
  // Only 'authenticated' in the mix
145
167
  if (hasAuthenticated) {
146
168
  return `if (!context.request.user) {
147
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
169
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
148
170
  }`;
149
171
  }
150
172
  return '';
@@ -168,10 +190,10 @@ class ControllerGenerator {
168
190
  // Owner validation (post-fetch for reads, via parent)
169
191
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
170
192
  if (resourceOwnerId === null) {
171
- throw new Error('Resource not found');
193
+ throw new NotFoundError('Resource not found');
172
194
  }
173
195
  if (resourceOwnerId !== context.request.user?.id) {
174
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
196
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
175
197
  }`;
176
198
  }
177
199
  const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
@@ -179,12 +201,12 @@ class ControllerGenerator {
179
201
  // Owner validation (post-fetch for reads, via parent, bypassed for: ${bypassRoles.join(', ')})
180
202
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
181
203
  if (resourceOwnerId === null) {
182
- throw new Error('Resource not found');
204
+ throw new NotFoundError('Resource not found');
183
205
  }
184
206
  const isOwner = resourceOwnerId === context.request.user?.id;
185
207
  const hasPrivilegedRole = ${bypassConditions};
186
208
  if (!isOwner && !hasPrivilegedRole) {
187
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
209
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
188
210
  }`;
189
211
  }
190
212
  // Root entity: result has ownerId
@@ -192,7 +214,7 @@ class ControllerGenerator {
192
214
  return `
193
215
  // Owner validation (post-fetch for reads)
194
216
  if (${resultVar}.ownerId !== context.request.user?.id) {
195
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
217
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
196
218
  }`;
197
219
  }
198
220
  const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
@@ -201,7 +223,7 @@ class ControllerGenerator {
201
223
  const isOwner = ${resultVar}.ownerId === context.request.user?.id;
202
224
  const hasPrivilegedRole = ${bypassConditions};
203
225
  if (!isOwner && !hasPrivilegedRole) {
204
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
226
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
205
227
  }`;
206
228
  }
207
229
  /**
@@ -225,10 +247,10 @@ class ControllerGenerator {
225
247
  // Pre-mutation owner validation
226
248
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
227
249
  if (resourceOwnerId === null) {
228
- throw new Error('Resource not found');
250
+ throw new NotFoundError('Resource not found');
229
251
  }
230
252
  if (resourceOwnerId !== context.request.user?.id) {
231
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
253
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
232
254
  }
233
255
  `;
234
256
  }
@@ -238,12 +260,12 @@ class ControllerGenerator {
238
260
  // Pre-mutation owner validation (bypassed for: ${bypassRoles.join(', ')})
239
261
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
240
262
  if (resourceOwnerId === null) {
241
- throw new Error('Resource not found');
263
+ throw new NotFoundError('Resource not found');
242
264
  }
243
265
  const isOwner = resourceOwnerId === context.request.user?.id;
244
266
  const hasPrivilegedRole = ${bypassConditions};
245
267
  if (!isOwner && !hasPrivilegedRole) {
246
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
268
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
247
269
  }
248
270
  `;
249
271
  }
@@ -559,7 +581,9 @@ class ControllerGenerator {
559
581
  const constructorParams = Array.from(useCaseModels)
560
582
  .map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
561
583
  .join(',\n ');
562
- return `import { Controller, Get, Post, Put, Delete, type IContext } from '@currentjs/router';
584
+ const errorImports = this.getNeededHttpErrorImports(sortedEndpoints.map(e => e.auth));
585
+ const routerImports = ['Controller', 'Get', 'Post', 'Put', 'Delete', 'type IContext', ...errorImports].join(', ');
586
+ return `import { ${routerImports} } from '@currentjs/router';
563
587
  ${useCaseImports}
564
588
  ${dtoImportStatements}
565
589
 
@@ -626,7 +650,9 @@ ${methods.join('\n\n')}
626
650
  ${constructorParams.join(',\n ')}
627
651
  ) {}`
628
652
  : 'constructor() {}';
629
- return `import { Controller, Get, Post, Render, type IContext } from '@currentjs/router';
653
+ const errorImports = this.getNeededHttpErrorImports(sortedPages.map(p => p.auth));
654
+ const routerImports = ['Controller', 'Get', 'Post', 'Render', 'type IContext', ...errorImports].join(', ');
655
+ return `import { ${routerImports} } from '@currentjs/router';
630
656
  ${useCaseImports}
631
657
  ${serviceImports.join('\n')}
632
658
  ${dtoImportStatements}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@currentjs/gen",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "CLI code generator",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "Konstantin Zavalny",