create-velox-app 0.6.73 → 0.6.74

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,11 @@
1
1
  # create-velox-app
2
2
 
3
+ ## 0.6.74
4
+
5
+ ### Patch Changes
6
+
7
+ - docs: fix erroneous file paths and improve auth context documentation
8
+
3
9
  ## 0.6.73
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-velox-app",
3
- "version": "0.6.73",
3
+ "version": "0.6.74",
4
4
  "description": "Project scaffolder for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -99,3 +99,38 @@ export const jwt = jwtManager({
99
99
  issuer: 'velox-app',
100
100
  audience: 'velox-app-client',
101
101
  });
102
+
103
+ // ============================================================================
104
+ // Full User Helper
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Fetch full user from database when ctx.user doesn't have all fields.
109
+ *
110
+ * `ctx.user` only contains fields returned by `userLoader` (id, email, name, roles).
111
+ * Use this helper when you need additional fields like `organizationId`.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // In a procedure that needs full user data:
116
+ * .query(async ({ ctx }) => {
117
+ * // ctx.user has: id, email, name, roles
118
+ * // For additional fields, fetch full user:
119
+ * const fullUser = await getFullUser(ctx);
120
+ * return { organizationId: fullUser.organizationId };
121
+ * })
122
+ * ```
123
+ *
124
+ * @param ctx - The procedure context (must have user.id and db)
125
+ * @returns The full user record from database
126
+ * @throws If user is not found (should not happen for authenticated requests)
127
+ */
128
+ export async function getFullUser<
129
+ TDb extends {
130
+ user: { findUniqueOrThrow: (args: { where: { id: string } }) => Promise<unknown> };
131
+ },
132
+ >(ctx: { user: { id: string }; db: TDb }) {
133
+ return ctx.db.user.findUniqueOrThrow({
134
+ where: { id: ctx.user.id },
135
+ });
136
+ }
@@ -76,18 +76,24 @@ getUser: procedure().input(z.object({ id: z.string() }))
76
76
 
77
77
  **Cause**: Procedure not registered in router.
78
78
 
79
- **Fix**:
80
- 1. Check `src/procedures/index.ts` exports your procedure
81
- 2. Check `src/index.ts` includes it in collections array
79
+ **Fix**: Add procedure to `createRouter()` in `src/router.ts`:
82
80
 
83
81
  ```typescript
84
- // src/procedures/index.ts
85
- export * from './users.js';
86
- export * from './posts.js'; // Add this
82
+ // src/router.ts
83
+ import { createRouter, extractRoutes } from '@veloxts/velox';
84
+
85
+ import { healthProcedures } from './procedures/health.js';
86
+ import { userProcedures } from './procedures/users.js';
87
+ import { postProcedures } from './procedures/posts.js'; // Add import
88
+
89
+ export const { collections, router } = createRouter(
90
+ healthProcedures,
91
+ userProcedures,
92
+ postProcedures // Add here
93
+ );
87
94
 
88
- // src/index.ts
89
- import { userProcedures, postProcedures } from './procedures/index.js';
90
- const collections = [userProcedures, postProcedures]; // Add here
95
+ export type AppRouter = typeof router;
96
+ export const routes = extractRoutes(collections);
91
97
  ```
92
98
 
93
99
  ### "Input validation failed"
@@ -67,7 +67,19 @@ export const postProcedures = procedures('posts', {
67
67
  });
68
68
  ```
69
69
 
70
- Then register in `src/procedures/index.ts` and add to collections in `src/index.ts`.
70
+ Then register in `src/router.ts` by importing and adding to `createRouter()`:
71
+
72
+ ```typescript
73
+ // src/router.ts
74
+ import { postProcedures } from './procedures/posts.js';
75
+
76
+ export const { collections, router } = createRouter(
77
+ healthProcedures,
78
+ authProcedures,
79
+ userProcedures,
80
+ postProcedures // Add here
81
+ );
82
+ ```
71
83
 
72
84
  ## Prisma 7 Configuration
73
85
 
@@ -229,19 +241,41 @@ createdAt: z.coerce.date()
229
241
  updatedAt: z.coerce.date()
230
242
  ```
231
243
 
232
- ### Extending User Context
244
+ ### Authentication Context (`ctx.user`)
233
245
 
234
- The `ctx.user` object is populated by `userLoader` in `src/config/auth.ts`.
246
+ **Important:** `ctx.user` is NOT the raw database user - it only contains fields explicitly returned by `userLoader` in `src/config/auth.ts`.
235
247
 
236
- To add fields to `ctx.user` (e.g., `organizationId`):
248
+ #### How ctx.user Gets Populated
237
249
 
238
- 1. Update `userLoader`:
250
+ 1. JWT token is validated from cookie/header
251
+ 2. User ID is extracted from token payload
252
+ 3. `userLoader(userId)` is called to fetch user data
253
+ 4. Only the fields returned by `userLoader` are available on `ctx.user`
254
+
255
+ #### Default Fields
256
+
257
+ The default `userLoader` returns:
258
+ ```typescript
259
+ {
260
+ id: string;
261
+ email: string;
262
+ name: string;
263
+ roles: string[];
264
+ }
265
+ ```
266
+
267
+ #### Adding Fields to ctx.user
268
+
269
+ To add a field like `organizationId`:
270
+
271
+ 1. Update `userLoader` in `src/config/auth.ts`:
239
272
  ```typescript
240
273
  async function userLoader(userId: string) {
241
274
  const user = await db.user.findUnique({ where: { id: userId } });
242
275
  return {
243
276
  id: user.id,
244
277
  email: user.email,
278
+ name: user.name,
245
279
  roles: parseUserRoles(user.roles),
246
280
  organizationId: user.organizationId, // Add new fields here
247
281
  };
@@ -250,15 +284,36 @@ async function userLoader(userId: string) {
250
284
 
251
285
  2. Update related schemas (`UserSchema`, `UpdateUserInput`, etc.).
252
286
 
287
+ #### Common Mistake
288
+
289
+ ```typescript
290
+ // ❌ WRONG - organizationId is undefined (not in userLoader)
291
+ const orgId = ctx.user.organizationId;
292
+
293
+ // ✅ CORRECT - After adding to userLoader in src/config/auth.ts
294
+ const orgId = ctx.user.organizationId;
295
+
296
+ // ✅ ALTERNATIVE - Use getFullUser helper when you need extra fields
297
+ import { getFullUser } from '@/utils/auth';
298
+ const fullUser = await getFullUser(ctx);
299
+ const orgId = fullUser.organizationId;
300
+ ```
301
+
253
302
  ### Role Configuration
254
303
 
255
- Roles are stored as a JSON string array in the database (e.g., `["ADMIN"]`).
304
+ Roles are stored as a JSON string array in the database (e.g., `["user"]`, `["admin"]`).
305
+
306
+ The `parseUserRoles()` function from `@veloxts/auth` safely parses the JSON string:
307
+ ```typescript
308
+ // Imported and used in src/config/auth.ts
309
+ import { parseUserRoles } from '@veloxts/auth';
310
+ roles: parseUserRoles(user.roles), // Converts '["user"]' to ['user']
311
+ ```
256
312
 
257
- When changing roles, update ALL locations:
258
- - `prisma/schema.prisma` - Role enum
259
- - `src/config/auth.ts` - `ALLOWED_ROLES` and `parseUserRoles`
260
- - `src/utils/auth.ts` - `ALLOWED_ROLES` (if duplicated)
261
- - `src/schemas/auth.ts` - Role validation schemas
313
+ When adding new roles, update:
314
+ - `prisma/schema.prisma` - Default value in the roles field
315
+ - `src/schemas/auth.ts` - Role validation schemas (if using enum validation)
316
+ - Any guards that check for specific roles (e.g., `hasRole('admin')`)
262
317
 
263
318
  ### MCP Project Path
264
319
 
@@ -66,7 +66,18 @@ export const postProcedures = procedures('posts', {
66
66
  });
67
67
  ```
68
68
 
69
- Then register in `src/procedures/index.ts` and add to collections in `src/index.ts`.
69
+ Then register in `src/router.ts` by importing and adding to `createRouter()`:
70
+
71
+ ```typescript
72
+ // src/router.ts
73
+ import { postProcedures } from './procedures/posts.js';
74
+
75
+ export const { collections, router } = createRouter(
76
+ healthProcedures,
77
+ userProcedures,
78
+ postProcedures // Add here
79
+ );
80
+ ```
70
81
 
71
82
  ## Prisma 7 Configuration
72
83
 
@@ -122,6 +122,44 @@ await logout(); // Clears cookies server-side
122
122
  - At least one number
123
123
  - Not a common password
124
124
 
125
+ ### Authentication Context (`ctx.user`)
126
+
127
+ **Important:** `ctx.user` is NOT the raw database user - it only contains fields explicitly returned by `userLoader` in `src/api/handler.ts`.
128
+
129
+ #### How ctx.user Gets Populated
130
+
131
+ 1. JWT token is validated from cookie/header
132
+ 2. User ID is extracted from token payload
133
+ 3. `userLoader(userId)` is called to fetch user data
134
+ 4. Only the fields returned by `userLoader` are available on `ctx.user`
135
+
136
+ #### Default Fields
137
+
138
+ The default `userLoader` returns:
139
+ ```typescript
140
+ {
141
+ id: string;
142
+ email: string;
143
+ name: string;
144
+ roles: string[];
145
+ }
146
+ ```
147
+
148
+ #### Common Mistake
149
+
150
+ ```typescript
151
+ // ❌ WRONG - organizationId is undefined (not in userLoader)
152
+ const orgId = ctx.user.organizationId;
153
+
154
+ // ✅ CORRECT - After adding to userLoader in src/api/handler.ts
155
+ const orgId = ctx.user.organizationId;
156
+
157
+ // ✅ ALTERNATIVE - Use getFullUser helper when you need extra fields
158
+ import { getFullUser } from '@/api/utils/auth';
159
+ const fullUser = await getFullUser(ctx);
160
+ const orgId = fullUser.organizationId;
161
+ ```
162
+
125
163
  ## Server Actions with validated()
126
164
 
127
165
  Use the `validated()` helper for secure server actions:
@@ -302,33 +340,41 @@ updatedAt: z.coerce.date()
302
340
 
303
341
  ### Extending User Context
304
342
 
305
- The `ctx.user` object is populated by `userLoader` in `src/api/utils/auth.ts`.
343
+ The `ctx.user` object is populated by `userLoader` in `src/api/handler.ts` (inline in `createAuthConfig()`).
306
344
 
307
345
  To add fields to `ctx.user` (e.g., `organizationId`):
308
346
 
309
- 1. Update `userLoader`:
347
+ 1. Update `userLoader` in `src/api/handler.ts`:
310
348
  ```typescript
311
- async function userLoader(userId: string) {
349
+ userLoader: async (userId: string) => {
312
350
  const user = await db.user.findUnique({ where: { id: userId } });
313
351
  return {
314
352
  id: user.id,
315
353
  email: user.email,
354
+ name: user.name,
316
355
  roles: parseUserRoles(user.roles),
317
356
  organizationId: user.organizationId, // Add new fields here
318
357
  };
319
- }
358
+ },
320
359
  ```
321
360
 
322
361
  2. Update related schemas (`UserSchema`, `UpdateUserInput`, etc.).
323
362
 
324
363
  ### Role Configuration
325
364
 
326
- Roles are stored as a JSON string array in the database (e.g., `["ADMIN"]`).
365
+ Roles are stored as a JSON string array in the database (e.g., `["user"]`, `["admin"]`).
366
+
367
+ The `parseUserRoles()` function from `@veloxts/auth` safely parses the JSON string:
368
+ ```typescript
369
+ // Imported and used in src/api/handler.ts
370
+ import { parseUserRoles } from '@veloxts/auth';
371
+ roles: parseUserRoles(user.roles), // Converts '["user"]' to ['user']
372
+ ```
327
373
 
328
- When changing roles, update ALL locations:
329
- - `prisma/schema.prisma` - Role enum
330
- - `src/api/utils/auth.ts` - `ALLOWED_ROLES` and `parseUserRoles`
331
- - `src/api/schemas/auth.ts` - Role validation schemas
374
+ When adding new roles, update:
375
+ - `prisma/schema.prisma` - Default value in the roles field
376
+ - `src/api/schemas/auth.ts` - Role validation schemas (if using enum validation)
377
+ - Any guards or server actions that check for specific roles
332
378
 
333
379
  ### MCP Project Path
334
380
 
@@ -99,3 +99,38 @@ export const jwt = jwtManager({
99
99
  issuer: 'velox-app',
100
100
  audience: 'velox-app-client',
101
101
  });
102
+
103
+ // ============================================================================
104
+ // Full User Helper
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Fetch full user from database when ctx.user doesn't have all fields.
109
+ *
110
+ * `ctx.user` only contains fields returned by `userLoader` (id, email, name, roles).
111
+ * Use this helper when you need additional fields like `organizationId`.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // In a procedure that needs full user data:
116
+ * .query(async ({ ctx }) => {
117
+ * // ctx.user has: id, email, name, roles
118
+ * // For additional fields, fetch full user:
119
+ * const fullUser = await getFullUser(ctx);
120
+ * return { organizationId: fullUser.organizationId };
121
+ * })
122
+ * ```
123
+ *
124
+ * @param ctx - The procedure context (must have user.id and db)
125
+ * @returns The full user record from database
126
+ * @throws If user is not found (should not happen for authenticated requests)
127
+ */
128
+ export async function getFullUser<
129
+ TDb extends {
130
+ user: { findUniqueOrThrow: (args: { where: { id: string } }) => Promise<unknown> };
131
+ },
132
+ >(ctx: { user: { id: string }; db: TDb }) {
133
+ return ctx.db.user.findUniqueOrThrow({
134
+ where: { id: ctx.user.id },
135
+ });
136
+ }