create-velox-app 0.6.64 → 0.6.65
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 +6 -0
- package/dist/templates/auth.js +0 -8
- package/dist/templates/shared/root.d.ts +7 -0
- package/dist/templates/shared/root.js +32 -0
- package/dist/templates/spa.js +0 -8
- package/dist/templates/trpc.js +0 -8
- package/package.json +1 -1
- package/src/templates/source/root/.claude/skills/veloxts/GENERATORS.md +313 -0
- package/src/templates/source/root/.claude/skills/veloxts/PROCEDURES.md +466 -0
- package/src/templates/source/root/.claude/skills/veloxts/SKILL.md +225 -0
- package/src/templates/source/root/.claude/skills/veloxts/TROUBLESHOOTING.md +416 -0
- package/src/templates/source/root/CLAUDE.auth.md +33 -1
- package/src/templates/source/root/CLAUDE.default.md +33 -1
- package/src/templates/source/root/CLAUDE.trpc.md +20 -0
- package/src/templates/source/rsc/CLAUDE.md +19 -0
- package/src/templates/source/rsc-auth/CLAUDE.md +19 -0
- package/src/templates/source/web/api.ts +4 -5
- package/src/templates/source/web/main.tsx +2 -2
- package/src/templates/source/web/routes/__root.tsx +1 -1
- package/src/templates/source/api/router.types.auth.ts +0 -88
- package/src/templates/source/api/router.types.default.ts +0 -73
- package/src/templates/source/api/router.types.trpc.ts +0 -73
- package/src/templates/source/api/routes.auth.ts +0 -66
- package/src/templates/source/api/routes.default.ts +0 -53
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# VeloxTS Procedures API Reference
|
|
2
|
+
|
|
3
|
+
Procedures are the core abstraction for defining type-safe API endpoints. They combine validation, handlers, and routing in a fluent builder pattern.
|
|
4
|
+
|
|
5
|
+
## Basic Structure
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { procedure, procedures, z } from '@veloxts/velox';
|
|
9
|
+
|
|
10
|
+
export const userProcedures = procedures('users', {
|
|
11
|
+
getUser: procedure()
|
|
12
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
13
|
+
.output(UserSchema)
|
|
14
|
+
.query(async ({ input, ctx }) => {
|
|
15
|
+
return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
createUser: procedure()
|
|
19
|
+
.input(CreateUserSchema)
|
|
20
|
+
.output(UserSchema)
|
|
21
|
+
.mutation(async ({ input, ctx }) => {
|
|
22
|
+
return ctx.db.user.create({ data: input });
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Procedure Methods
|
|
28
|
+
|
|
29
|
+
### `.input(schema)`
|
|
30
|
+
|
|
31
|
+
Validates incoming request data with Zod.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
.input(z.object({
|
|
35
|
+
id: z.string().uuid(),
|
|
36
|
+
email: z.string().email(),
|
|
37
|
+
age: z.number().min(0).max(150).optional(),
|
|
38
|
+
}))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Important**: Input is validated before the handler runs. Invalid input returns 400.
|
|
42
|
+
|
|
43
|
+
### `.output(schema)`
|
|
44
|
+
|
|
45
|
+
Validates and types the response.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
.output(z.object({
|
|
49
|
+
id: z.string(),
|
|
50
|
+
name: z.string(),
|
|
51
|
+
createdAt: z.coerce.date(),
|
|
52
|
+
}))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Tip**: Use `.output()` for type inference and runtime validation of responses.
|
|
56
|
+
|
|
57
|
+
### `.query(handler)` vs `.mutation(handler)`
|
|
58
|
+
|
|
59
|
+
- **Query**: Read operations (GET requests)
|
|
60
|
+
- **Mutation**: Write operations (POST, PUT, PATCH, DELETE)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Query - for reading data
|
|
64
|
+
listUsers: procedure()
|
|
65
|
+
.output(z.array(UserSchema))
|
|
66
|
+
.query(async ({ ctx }) => {
|
|
67
|
+
return ctx.db.user.findMany();
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
// Mutation - for writing data
|
|
71
|
+
createUser: procedure()
|
|
72
|
+
.input(CreateUserSchema)
|
|
73
|
+
.output(UserSchema)
|
|
74
|
+
.mutation(async ({ input, ctx }) => {
|
|
75
|
+
return ctx.db.user.create({ data: input });
|
|
76
|
+
}),
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `.guard(guardFn)`
|
|
80
|
+
|
|
81
|
+
Protects procedures with authentication/authorization.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { authenticated, hasRole, hasPermission } from '@veloxts/auth';
|
|
85
|
+
|
|
86
|
+
// Require authentication
|
|
87
|
+
getProfile: procedure()
|
|
88
|
+
.guard(authenticated)
|
|
89
|
+
.query(({ ctx }) => ctx.user),
|
|
90
|
+
|
|
91
|
+
// Require specific role
|
|
92
|
+
adminPanel: procedure()
|
|
93
|
+
.guard(hasRole('admin'))
|
|
94
|
+
.query(() => ({ admin: true })),
|
|
95
|
+
|
|
96
|
+
// Require permission
|
|
97
|
+
deletePost: procedure()
|
|
98
|
+
.guard(hasPermission('posts.delete'))
|
|
99
|
+
.input(z.object({ id: z.string() }))
|
|
100
|
+
.mutation(async ({ ctx, input }) => {
|
|
101
|
+
await ctx.db.post.delete({ where: { id: input.id } });
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
// Custom guard
|
|
105
|
+
isOwner: procedure()
|
|
106
|
+
.guard(async ({ ctx, input }) => {
|
|
107
|
+
const post = await ctx.db.post.findUnique({ where: { id: input.id } });
|
|
108
|
+
if (post?.authorId !== ctx.user?.id) {
|
|
109
|
+
throw new Error('Not authorized');
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.input(z.object({ id: z.string() }))
|
|
113
|
+
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `.rest(options)`
|
|
117
|
+
|
|
118
|
+
Override automatic REST route inference.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Custom path
|
|
122
|
+
publishPost: procedure()
|
|
123
|
+
.input(z.object({ id: z.string() }))
|
|
124
|
+
.rest({ method: 'POST', path: '/posts/:id/publish' })
|
|
125
|
+
.mutation(/* ... */),
|
|
126
|
+
|
|
127
|
+
// Custom method
|
|
128
|
+
archivePost: procedure()
|
|
129
|
+
.rest({ method: 'PATCH', path: '/posts/:id/archive' })
|
|
130
|
+
.mutation(/* ... */),
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Warning**: Don't include `/api` prefix - it's added automatically.
|
|
134
|
+
|
|
135
|
+
### `.use(middleware)`
|
|
136
|
+
|
|
137
|
+
Add procedure-specific middleware.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { rateLimit, cache } from '@veloxts/velox';
|
|
141
|
+
|
|
142
|
+
// Rate limiting
|
|
143
|
+
createComment: procedure()
|
|
144
|
+
.use(rateLimit({ max: 10, window: '1m' }))
|
|
145
|
+
.input(CommentSchema)
|
|
146
|
+
.mutation(/* ... */),
|
|
147
|
+
|
|
148
|
+
// Caching
|
|
149
|
+
getPopularPosts: procedure()
|
|
150
|
+
.use(cache({ ttl: '5m' }))
|
|
151
|
+
.output(z.array(PostSchema))
|
|
152
|
+
.query(/* ... */),
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## REST Route Inference
|
|
156
|
+
|
|
157
|
+
VeloxTS automatically maps procedure names to HTTP routes:
|
|
158
|
+
|
|
159
|
+
### Query Procedures (GET)
|
|
160
|
+
|
|
161
|
+
| Prefix | HTTP | Route Pattern | Example |
|
|
162
|
+
|--------|------|---------------|---------|
|
|
163
|
+
| `get*` | GET | `/{namespace}/:id` | `getUser` → `GET /users/:id` |
|
|
164
|
+
| `list*` | GET | `/{namespace}` | `listUsers` → `GET /users` |
|
|
165
|
+
| `find*` | GET | `/{namespace}` | `findUsers` → `GET /users` |
|
|
166
|
+
|
|
167
|
+
### Mutation Procedures
|
|
168
|
+
|
|
169
|
+
| Prefix | HTTP | Route Pattern | Example |
|
|
170
|
+
|--------|------|---------------|---------|
|
|
171
|
+
| `create*` | POST | `/{namespace}` (201) | `createUser` → `POST /users` |
|
|
172
|
+
| `add*` | POST | `/{namespace}` (201) | `addUser` → `POST /users` |
|
|
173
|
+
| `update*` | PUT | `/{namespace}/:id` | `updateUser` → `PUT /users/:id` |
|
|
174
|
+
| `edit*` | PUT | `/{namespace}/:id` | `editUser` → `PUT /users/:id` |
|
|
175
|
+
| `patch*` | PATCH | `/{namespace}/:id` | `patchUser` → `PATCH /users/:id` |
|
|
176
|
+
| `delete*` | DELETE | `/{namespace}/:id` | `deleteUser` → `DELETE /users/:id` |
|
|
177
|
+
| `remove*` | DELETE | `/{namespace}/:id` | `removeUser` → `DELETE /users/:id` |
|
|
178
|
+
|
|
179
|
+
### React Query Hook Mapping
|
|
180
|
+
|
|
181
|
+
Naming also determines which React Query hooks are available:
|
|
182
|
+
|
|
183
|
+
**Query prefixes** (`get*`, `list*`, `find*`):
|
|
184
|
+
```typescript
|
|
185
|
+
api.users.getUser.useQuery({ id })
|
|
186
|
+
api.users.listUsers.useSuspenseQuery({})
|
|
187
|
+
api.posts.findPosts.useQuery({ search: 'hello' })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Mutation prefixes** (everything else):
|
|
191
|
+
```typescript
|
|
192
|
+
api.users.createUser.useMutation()
|
|
193
|
+
api.posts.updatePost.useMutation()
|
|
194
|
+
api.comments.deleteComment.useMutation()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Warning**: Non-standard names like `fetchUsers` are treated as mutations!
|
|
198
|
+
|
|
199
|
+
## Context Object
|
|
200
|
+
|
|
201
|
+
The `ctx` parameter provides request-scoped data:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
.query(async ({ input, ctx }) => {
|
|
205
|
+
// Database client
|
|
206
|
+
const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
207
|
+
|
|
208
|
+
// Current authenticated user (if using auth)
|
|
209
|
+
const currentUser = ctx.user;
|
|
210
|
+
|
|
211
|
+
// Raw Fastify request/reply
|
|
212
|
+
const ip = ctx.request.ip;
|
|
213
|
+
ctx.reply.header('X-Custom', 'value');
|
|
214
|
+
|
|
215
|
+
// Cache (if configured)
|
|
216
|
+
const cached = await ctx.cache.get('key');
|
|
217
|
+
|
|
218
|
+
// Queue (if configured)
|
|
219
|
+
await ctx.queue.dispatch(SendEmailJob, { to: user.email });
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Available Context Properties
|
|
224
|
+
|
|
225
|
+
| Property | Type | Description |
|
|
226
|
+
|----------|------|-------------|
|
|
227
|
+
| `ctx.db` | `PrismaClient` | Database client |
|
|
228
|
+
| `ctx.user` | `User \| undefined` | Authenticated user |
|
|
229
|
+
| `ctx.request` | `FastifyRequest` | Raw HTTP request |
|
|
230
|
+
| `ctx.reply` | `FastifyReply` | Raw HTTP response |
|
|
231
|
+
| `ctx.cache` | `CacheManager` | Cache operations (if enabled) |
|
|
232
|
+
| `ctx.queue` | `QueueManager` | Job queue (if enabled) |
|
|
233
|
+
| `ctx.storage` | `StorageManager` | File storage (if enabled) |
|
|
234
|
+
|
|
235
|
+
## Validation Patterns
|
|
236
|
+
|
|
237
|
+
### Input Schemas
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Required fields
|
|
241
|
+
const CreateUserInput = z.object({
|
|
242
|
+
email: z.string().email(),
|
|
243
|
+
name: z.string().min(1).max(255),
|
|
244
|
+
password: z.string().min(8),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Optional fields with defaults
|
|
248
|
+
const ListUsersInput = z.object({
|
|
249
|
+
page: z.number().min(1).default(1),
|
|
250
|
+
limit: z.number().min(1).max(100).default(10),
|
|
251
|
+
search: z.string().optional(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Enum fields
|
|
255
|
+
const UpdateStatusInput = z.object({
|
|
256
|
+
status: z.enum(['draft', 'published', 'archived']),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Nested objects
|
|
260
|
+
const CreateOrderInput = z.object({
|
|
261
|
+
items: z.array(z.object({
|
|
262
|
+
productId: z.string().uuid(),
|
|
263
|
+
quantity: z.number().min(1),
|
|
264
|
+
})).min(1),
|
|
265
|
+
shippingAddress: AddressSchema,
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Output Schemas
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Handle Prisma types
|
|
273
|
+
const UserSchema = z.object({
|
|
274
|
+
id: z.string().uuid(),
|
|
275
|
+
email: z.string(),
|
|
276
|
+
name: z.string(),
|
|
277
|
+
// Prisma returns Date objects
|
|
278
|
+
createdAt: z.coerce.date(),
|
|
279
|
+
updatedAt: z.coerce.date(),
|
|
280
|
+
// Prisma Decimal → number
|
|
281
|
+
balance: z.any().transform((val) => Number(val)),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Nullable response
|
|
285
|
+
.output(UserSchema.nullable())
|
|
286
|
+
|
|
287
|
+
// Array response
|
|
288
|
+
.output(z.array(UserSchema))
|
|
289
|
+
|
|
290
|
+
// Paginated response
|
|
291
|
+
.output(z.object({
|
|
292
|
+
data: z.array(UserSchema),
|
|
293
|
+
meta: z.object({
|
|
294
|
+
page: z.number(),
|
|
295
|
+
limit: z.number(),
|
|
296
|
+
total: z.number(),
|
|
297
|
+
totalPages: z.number(),
|
|
298
|
+
}),
|
|
299
|
+
}))
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Error Handling
|
|
303
|
+
|
|
304
|
+
### VeloxError
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { VeloxError } from '@veloxts/core';
|
|
308
|
+
|
|
309
|
+
getUser: procedure()
|
|
310
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
311
|
+
.query(async ({ input, ctx }) => {
|
|
312
|
+
const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
313
|
+
|
|
314
|
+
if (!user) {
|
|
315
|
+
throw VeloxError.notFound('User', input.id);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return user;
|
|
319
|
+
}),
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Error Types
|
|
323
|
+
|
|
324
|
+
| Method | HTTP | Use Case |
|
|
325
|
+
|--------|------|----------|
|
|
326
|
+
| `VeloxError.notFound(entity, id)` | 404 | Resource not found |
|
|
327
|
+
| `VeloxError.unauthorized(message)` | 401 | Not authenticated |
|
|
328
|
+
| `VeloxError.forbidden(message)` | 403 | Not authorized |
|
|
329
|
+
| `VeloxError.badRequest(message)` | 400 | Invalid input |
|
|
330
|
+
| `VeloxError.conflict(message)` | 409 | Duplicate resource |
|
|
331
|
+
| `VeloxError.internal(message)` | 500 | Server error |
|
|
332
|
+
|
|
333
|
+
## Complete Example
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { procedure, procedures, paginationInputSchema, z } from '@veloxts/velox';
|
|
337
|
+
import { authenticated, hasRole } from '@veloxts/auth';
|
|
338
|
+
import { VeloxError } from '@veloxts/core';
|
|
339
|
+
|
|
340
|
+
// Schemas
|
|
341
|
+
const PostSchema = z.object({
|
|
342
|
+
id: z.string().uuid(),
|
|
343
|
+
title: z.string(),
|
|
344
|
+
content: z.string(),
|
|
345
|
+
authorId: z.string().uuid(),
|
|
346
|
+
publishedAt: z.coerce.date().nullable(),
|
|
347
|
+
createdAt: z.coerce.date(),
|
|
348
|
+
updatedAt: z.coerce.date(),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const CreatePostInput = z.object({
|
|
352
|
+
title: z.string().min(1).max(255),
|
|
353
|
+
content: z.string().min(1),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const UpdatePostInput = CreatePostInput.partial();
|
|
357
|
+
|
|
358
|
+
// Procedures
|
|
359
|
+
export const postProcedures = procedures('posts', {
|
|
360
|
+
// Public: list published posts
|
|
361
|
+
listPosts: procedure()
|
|
362
|
+
.input(paginationInputSchema.optional())
|
|
363
|
+
.output(z.object({
|
|
364
|
+
data: z.array(PostSchema),
|
|
365
|
+
meta: z.object({
|
|
366
|
+
page: z.number(),
|
|
367
|
+
limit: z.number(),
|
|
368
|
+
total: z.number(),
|
|
369
|
+
totalPages: z.number(),
|
|
370
|
+
}),
|
|
371
|
+
}))
|
|
372
|
+
.query(async ({ input, ctx }) => {
|
|
373
|
+
const page = input?.page ?? 1;
|
|
374
|
+
const limit = input?.limit ?? 10;
|
|
375
|
+
const skip = (page - 1) * limit;
|
|
376
|
+
|
|
377
|
+
const where = { publishedAt: { not: null } };
|
|
378
|
+
|
|
379
|
+
const [data, total] = await Promise.all([
|
|
380
|
+
ctx.db.post.findMany({ where, skip, take: limit }),
|
|
381
|
+
ctx.db.post.count({ where }),
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
data,
|
|
386
|
+
meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
|
387
|
+
};
|
|
388
|
+
}),
|
|
389
|
+
|
|
390
|
+
// Public: get single post
|
|
391
|
+
getPost: procedure()
|
|
392
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
393
|
+
.output(PostSchema.nullable())
|
|
394
|
+
.query(async ({ input, ctx }) => {
|
|
395
|
+
return ctx.db.post.findUnique({ where: { id: input.id } });
|
|
396
|
+
}),
|
|
397
|
+
|
|
398
|
+
// Auth required: create post
|
|
399
|
+
createPost: procedure()
|
|
400
|
+
.guard(authenticated)
|
|
401
|
+
.input(CreatePostInput)
|
|
402
|
+
.output(PostSchema)
|
|
403
|
+
.mutation(async ({ input, ctx }) => {
|
|
404
|
+
return ctx.db.post.create({
|
|
405
|
+
data: {
|
|
406
|
+
...input,
|
|
407
|
+
authorId: ctx.user!.id,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}),
|
|
411
|
+
|
|
412
|
+
// Auth required: update own post
|
|
413
|
+
updatePost: procedure()
|
|
414
|
+
.guard(authenticated)
|
|
415
|
+
.input(z.object({ id: z.string().uuid() }).merge(UpdatePostInput))
|
|
416
|
+
.output(PostSchema)
|
|
417
|
+
.mutation(async ({ input, ctx }) => {
|
|
418
|
+
const { id, ...data } = input;
|
|
419
|
+
|
|
420
|
+
const post = await ctx.db.post.findUnique({ where: { id } });
|
|
421
|
+
|
|
422
|
+
if (!post) {
|
|
423
|
+
throw VeloxError.notFound('Post', id);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (post.authorId !== ctx.user!.id) {
|
|
427
|
+
throw VeloxError.forbidden('You can only edit your own posts');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return ctx.db.post.update({ where: { id }, data });
|
|
431
|
+
}),
|
|
432
|
+
|
|
433
|
+
// Admin only: delete any post
|
|
434
|
+
deletePost: procedure()
|
|
435
|
+
.guard(hasRole('admin'))
|
|
436
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
437
|
+
.output(z.object({ success: z.boolean() }))
|
|
438
|
+
.mutation(async ({ input, ctx }) => {
|
|
439
|
+
await ctx.db.post.delete({ where: { id: input.id } });
|
|
440
|
+
return { success: true };
|
|
441
|
+
}),
|
|
442
|
+
|
|
443
|
+
// Custom route: publish post
|
|
444
|
+
publishPost: procedure()
|
|
445
|
+
.guard(authenticated)
|
|
446
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
447
|
+
.output(PostSchema)
|
|
448
|
+
.rest({ method: 'POST', path: '/posts/:id/publish' })
|
|
449
|
+
.mutation(async ({ input, ctx }) => {
|
|
450
|
+
const post = await ctx.db.post.findUnique({ where: { id: input.id } });
|
|
451
|
+
|
|
452
|
+
if (!post) {
|
|
453
|
+
throw VeloxError.notFound('Post', input.id);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (post.authorId !== ctx.user!.id) {
|
|
457
|
+
throw VeloxError.forbidden('You can only publish your own posts');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return ctx.db.post.update({
|
|
461
|
+
where: { id: input.id },
|
|
462
|
+
data: { publishedAt: new Date() },
|
|
463
|
+
});
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
```
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: veloxts
|
|
3
|
+
description: VeloxTS framework assistant for building full-stack TypeScript APIs. Helps with procedures, generators (velox make), REST routes, authentication, validation, and common errors. Use when creating endpoints, adding features, debugging issues, or learning VeloxTS patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# VeloxTS Development Assistant
|
|
7
|
+
|
|
8
|
+
I help you build type-safe full-stack applications with VeloxTS. Ask me about:
|
|
9
|
+
|
|
10
|
+
- Creating API endpoints (procedures)
|
|
11
|
+
- Generating code (`velox make resource`, `namespace`, `procedure`)
|
|
12
|
+
- REST route inference from naming conventions
|
|
13
|
+
- Authentication and guards
|
|
14
|
+
- Validation with Zod schemas
|
|
15
|
+
- Troubleshooting common errors
|
|
16
|
+
|
|
17
|
+
## Quick Decision: Which Generator?
|
|
18
|
+
|
|
19
|
+
**"I want to create a new database entity"**
|
|
20
|
+
```bash
|
|
21
|
+
velox make resource Post # RECOMMENDED - creates everything
|
|
22
|
+
```
|
|
23
|
+
Creates: Prisma model + Zod schema + Procedures + Tests + Auto-registers
|
|
24
|
+
|
|
25
|
+
**"I have an existing Prisma model"**
|
|
26
|
+
```bash
|
|
27
|
+
velox make namespace Order # For existing models
|
|
28
|
+
```
|
|
29
|
+
Creates: Zod schema + Procedures (no Prisma injection)
|
|
30
|
+
|
|
31
|
+
**"I need a single endpoint"**
|
|
32
|
+
```bash
|
|
33
|
+
velox make procedure health # Quick single procedure
|
|
34
|
+
```
|
|
35
|
+
Creates: Procedure file with inline schemas
|
|
36
|
+
|
|
37
|
+
See [GENERATORS.md](GENERATORS.md) for detailed guidance.
|
|
38
|
+
|
|
39
|
+
## Procedure Naming = REST Routes
|
|
40
|
+
|
|
41
|
+
VeloxTS infers HTTP methods from procedure names:
|
|
42
|
+
|
|
43
|
+
| Name Pattern | HTTP | Route | Hook Type |
|
|
44
|
+
|--------------|------|-------|-----------|
|
|
45
|
+
| `getUser` | GET | `/users/:id` | `useQuery` |
|
|
46
|
+
| `listUsers` | GET | `/users` | `useQuery` |
|
|
47
|
+
| `findUsers` | GET | `/users` (search) | `useQuery` |
|
|
48
|
+
| `createUser` | POST | `/users` | `useMutation` |
|
|
49
|
+
| `updateUser` | PUT | `/users/:id` | `useMutation` |
|
|
50
|
+
| `patchUser` | PATCH | `/users/:id` | `useMutation` |
|
|
51
|
+
| `deleteUser` | DELETE | `/users/:id` | `useMutation` |
|
|
52
|
+
|
|
53
|
+
**Critical**: Non-standard names (like `fetchUsers`) are treated as mutations!
|
|
54
|
+
|
|
55
|
+
See [PROCEDURES.md](PROCEDURES.md) for the complete API reference.
|
|
56
|
+
|
|
57
|
+
## Common Tasks
|
|
58
|
+
|
|
59
|
+
### Create a New Resource
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Full CRUD with pagination
|
|
63
|
+
velox make resource BlogPost --crud --paginated
|
|
64
|
+
|
|
65
|
+
# With soft delete
|
|
66
|
+
velox make resource Comment --soft-delete
|
|
67
|
+
|
|
68
|
+
# Interactive field definition
|
|
69
|
+
velox make resource Product -i
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Add Authentication
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { authenticated, hasRole } from '@veloxts/auth';
|
|
76
|
+
|
|
77
|
+
// Require login
|
|
78
|
+
getProfile: procedure()
|
|
79
|
+
.guard(authenticated)
|
|
80
|
+
.query(({ ctx }) => ctx.user),
|
|
81
|
+
|
|
82
|
+
// Require admin role
|
|
83
|
+
deleteUser: procedure()
|
|
84
|
+
.guard(hasRole('admin'))
|
|
85
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
86
|
+
.mutation(async ({ ctx, input }) => {
|
|
87
|
+
await ctx.db.user.delete({ where: { id: input.id } });
|
|
88
|
+
return { success: true };
|
|
89
|
+
}),
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Add Pagination
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { paginationInputSchema } from '@veloxts/velox';
|
|
96
|
+
|
|
97
|
+
listPosts: procedure()
|
|
98
|
+
.input(paginationInputSchema.optional())
|
|
99
|
+
.output(z.object({
|
|
100
|
+
data: z.array(PostSchema),
|
|
101
|
+
meta: z.object({
|
|
102
|
+
page: z.number(),
|
|
103
|
+
limit: z.number(),
|
|
104
|
+
total: z.number(),
|
|
105
|
+
totalPages: z.number(),
|
|
106
|
+
}),
|
|
107
|
+
}))
|
|
108
|
+
.query(async ({ input, ctx }) => {
|
|
109
|
+
const page = input?.page ?? 1;
|
|
110
|
+
const limit = input?.limit ?? 10;
|
|
111
|
+
const skip = (page - 1) * limit;
|
|
112
|
+
|
|
113
|
+
const [data, total] = await Promise.all([
|
|
114
|
+
ctx.db.post.findMany({ skip, take: limit }),
|
|
115
|
+
ctx.db.post.count(),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
data,
|
|
120
|
+
meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Custom REST Route
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Override automatic inference
|
|
129
|
+
publishPost: procedure()
|
|
130
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
131
|
+
.output(PostSchema)
|
|
132
|
+
.rest({ method: 'POST', path: '/posts/:id/publish' }) // No /api prefix!
|
|
133
|
+
.mutation(async ({ ctx, input }) => {
|
|
134
|
+
return ctx.db.post.update({
|
|
135
|
+
where: { id: input.id },
|
|
136
|
+
data: { publishedAt: new Date() },
|
|
137
|
+
});
|
|
138
|
+
}),
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Troubleshooting
|
|
142
|
+
|
|
143
|
+
### "useQuery is not a function"
|
|
144
|
+
|
|
145
|
+
Your procedure name doesn't follow query conventions:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// BAD - "fetchUsers" is not a query prefix
|
|
149
|
+
const { data } = api.users.fetchUsers.useQuery({});
|
|
150
|
+
|
|
151
|
+
// GOOD - "listUsers" is a query prefix
|
|
152
|
+
const { data } = api.users.listUsers.useQuery({});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### "procedure.input is not a function"
|
|
156
|
+
|
|
157
|
+
Missing parentheses after `procedure`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// BAD
|
|
161
|
+
getUser: procedure.input(...)
|
|
162
|
+
|
|
163
|
+
// GOOD
|
|
164
|
+
getUser: procedure().input(...)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Prisma Decimal validation fails
|
|
168
|
+
|
|
169
|
+
Use transforms for decimal fields:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Input
|
|
173
|
+
price: z.coerce.number().positive()
|
|
174
|
+
|
|
175
|
+
// Output
|
|
176
|
+
price: z.any().transform((val) => Number(val))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for more solutions.
|
|
180
|
+
|
|
181
|
+
## Project Structure
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
apps/
|
|
185
|
+
├── api/ # Backend
|
|
186
|
+
│ ├── src/
|
|
187
|
+
│ │ ├── procedures/ # API endpoints (velox make procedure)
|
|
188
|
+
│ │ ├── schemas/ # Zod validation (velox make schema)
|
|
189
|
+
│ │ └── config/ # App configuration
|
|
190
|
+
│ └── prisma/
|
|
191
|
+
│ └── schema.prisma # Database schema
|
|
192
|
+
│
|
|
193
|
+
└── web/ # Frontend
|
|
194
|
+
└── src/
|
|
195
|
+
├── routes/ # Pages
|
|
196
|
+
└── components/ # UI components
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## CLI Quick Reference
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Development
|
|
203
|
+
pnpm dev # Start API (3030) + Web (8080) with HMR
|
|
204
|
+
pnpm velox dev --verbose # API only with timing metrics
|
|
205
|
+
|
|
206
|
+
# Database
|
|
207
|
+
pnpm db:push # Apply schema changes
|
|
208
|
+
pnpm db:studio # Open Prisma Studio
|
|
209
|
+
pnpm velox migrate status # Check migration status
|
|
210
|
+
|
|
211
|
+
# Code Generation
|
|
212
|
+
pnpm velox make resource Post --crud # Full resource
|
|
213
|
+
pnpm velox make namespace Order # Namespace + schema
|
|
214
|
+
pnpm velox make procedure health # Single procedure
|
|
215
|
+
|
|
216
|
+
# Seeding
|
|
217
|
+
pnpm velox db seed # Run all seeders
|
|
218
|
+
pnpm velox db seed --fresh # Truncate + seed
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Detailed Guides
|
|
222
|
+
|
|
223
|
+
- [GENERATORS.md](GENERATORS.md) - Complete generator reference with decision tree
|
|
224
|
+
- [PROCEDURES.md](PROCEDURES.md) - Procedure API, guards, context, validation
|
|
225
|
+
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Error messages and fixes
|