@veloxts/router 0.1.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.
- package/LICENSE +21 -0
- package/README.md +647 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/procedure/builder.d.ts +106 -0
- package/dist/procedure/builder.d.ts.map +1 -0
- package/dist/procedure/builder.js +341 -0
- package/dist/procedure/builder.js.map +1 -0
- package/dist/procedure/types.d.ts +234 -0
- package/dist/procedure/types.d.ts.map +1 -0
- package/dist/procedure/types.js +13 -0
- package/dist/procedure/types.js.map +1 -0
- package/dist/rest/adapter.d.ts +124 -0
- package/dist/rest/adapter.d.ts.map +1 -0
- package/dist/rest/adapter.js +262 -0
- package/dist/rest/adapter.js.map +1 -0
- package/dist/rest/index.d.ts +10 -0
- package/dist/rest/index.d.ts.map +1 -0
- package/dist/rest/index.js +9 -0
- package/dist/rest/index.js.map +1 -0
- package/dist/rest/naming.d.ts +83 -0
- package/dist/rest/naming.d.ts.map +1 -0
- package/dist/rest/naming.js +164 -0
- package/dist/rest/naming.js.map +1 -0
- package/dist/trpc/adapter.d.ts +183 -0
- package/dist/trpc/adapter.d.ts.map +1 -0
- package/dist/trpc/adapter.js +313 -0
- package/dist/trpc/adapter.js.map +1 -0
- package/dist/trpc/index.d.ts +8 -0
- package/dist/trpc/index.d.ts.map +1 -0
- package/dist/trpc/index.js +7 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 VeloxTS Framework Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# @veloxts/router
|
|
2
|
+
|
|
3
|
+
Procedure-based routing with hybrid tRPC and REST adapters for the VeloxTS framework.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @veloxts/router @veloxts/validation
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @veloxts/router @veloxts/validation
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { procedure, defineProcedures } from '@veloxts/router';
|
|
17
|
+
import { z } from '@veloxts/validation';
|
|
18
|
+
|
|
19
|
+
// Define procedures
|
|
20
|
+
export const userProcedures = defineProcedures('users', {
|
|
21
|
+
// GET /users/:id
|
|
22
|
+
getUser: procedure()
|
|
23
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
24
|
+
.output(UserSchema)
|
|
25
|
+
.query(async ({ input, ctx }) => {
|
|
26
|
+
return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
// POST /users
|
|
30
|
+
createUser: procedure()
|
|
31
|
+
.input(CreateUserSchema)
|
|
32
|
+
.output(UserSchema)
|
|
33
|
+
.mutation(async ({ input, ctx }) => {
|
|
34
|
+
return ctx.db.user.create({ data: input });
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Core Concepts
|
|
40
|
+
|
|
41
|
+
### Procedures
|
|
42
|
+
|
|
43
|
+
A **procedure** is the fundamental abstraction in VeloxTS. It defines:
|
|
44
|
+
|
|
45
|
+
1. **Input schema** - What data the endpoint expects (validated with Zod)
|
|
46
|
+
2. **Output schema** - What data the endpoint returns (validated with Zod)
|
|
47
|
+
3. **Handler** - The business logic (query for reads, mutation for writes)
|
|
48
|
+
4. **Middleware** - Optional request processing (auth, logging, etc.)
|
|
49
|
+
|
|
50
|
+
Procedures are type-safe from backend to frontend with zero code generation.
|
|
51
|
+
|
|
52
|
+
### Procedure Builder API
|
|
53
|
+
|
|
54
|
+
The fluent builder pattern enables composable, type-safe endpoint definitions:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
procedure()
|
|
58
|
+
.input(schema) // Define input validation
|
|
59
|
+
.output(schema) // Define output validation
|
|
60
|
+
.use(middleware) // Add middleware (optional)
|
|
61
|
+
.rest(config) // Override REST mapping (optional)
|
|
62
|
+
.query(handler) // Define read handler (GET)
|
|
63
|
+
// OR
|
|
64
|
+
.mutation(handler) // Define write handler (POST/PUT/DELETE)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Defining Procedures
|
|
68
|
+
|
|
69
|
+
### Query Procedures (Read Operations)
|
|
70
|
+
|
|
71
|
+
Use `.query()` for operations that read data:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const userProcedures = defineProcedures('users', {
|
|
75
|
+
// Get single user
|
|
76
|
+
getUser: procedure()
|
|
77
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
78
|
+
.output(UserSchema)
|
|
79
|
+
.query(async ({ input, ctx }) => {
|
|
80
|
+
const user = await ctx.db.user.findUnique({
|
|
81
|
+
where: { id: input.id },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!user) {
|
|
85
|
+
throw new NotFoundError('User', input.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return user;
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
// List users with pagination
|
|
92
|
+
listUsers: procedure()
|
|
93
|
+
.input(paginationInputSchema.optional())
|
|
94
|
+
.output(z.object({
|
|
95
|
+
data: z.array(UserSchema),
|
|
96
|
+
meta: z.object({
|
|
97
|
+
page: z.number(),
|
|
98
|
+
limit: z.number(),
|
|
99
|
+
total: z.number(),
|
|
100
|
+
}),
|
|
101
|
+
}))
|
|
102
|
+
.query(async ({ input, ctx }) => {
|
|
103
|
+
const page = input?.page ?? 1;
|
|
104
|
+
const limit = input?.limit ?? 10;
|
|
105
|
+
const skip = (page - 1) * limit;
|
|
106
|
+
|
|
107
|
+
const [data, total] = await Promise.all([
|
|
108
|
+
ctx.db.user.findMany({ skip, take: limit }),
|
|
109
|
+
ctx.db.user.count(),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
return { data, meta: { page, limit, total } };
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Mutation Procedures (Write Operations)
|
|
118
|
+
|
|
119
|
+
Use `.mutation()` for operations that modify data:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const userProcedures = defineProcedures('users', {
|
|
123
|
+
// Create user
|
|
124
|
+
createUser: procedure()
|
|
125
|
+
.input(z.object({
|
|
126
|
+
name: z.string().min(1),
|
|
127
|
+
email: z.string().email(),
|
|
128
|
+
}))
|
|
129
|
+
.output(UserSchema)
|
|
130
|
+
.mutation(async ({ input, ctx }) => {
|
|
131
|
+
return ctx.db.user.create({ data: input });
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
// Update user (v1.1+ - PUT endpoint)
|
|
135
|
+
updateUser: procedure()
|
|
136
|
+
.input(z.object({
|
|
137
|
+
id: z.string().uuid(),
|
|
138
|
+
name: z.string().min(1).optional(),
|
|
139
|
+
email: z.string().email().optional(),
|
|
140
|
+
}))
|
|
141
|
+
.output(UserSchema)
|
|
142
|
+
.mutation(async ({ input, ctx }) => {
|
|
143
|
+
const { id, ...data } = input;
|
|
144
|
+
return ctx.db.user.update({
|
|
145
|
+
where: { id },
|
|
146
|
+
data,
|
|
147
|
+
});
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## REST Endpoint Generation
|
|
153
|
+
|
|
154
|
+
VeloxTS automatically generates REST endpoints from procedure names using **naming conventions**:
|
|
155
|
+
|
|
156
|
+
### MVP (v0.1.0) - GET and POST
|
|
157
|
+
|
|
158
|
+
| Procedure Name | HTTP Method | REST Path | Notes |
|
|
159
|
+
|---------------|-------------|-----------|-------|
|
|
160
|
+
| `getUser` | GET | `/users/:id` | Single resource |
|
|
161
|
+
| `listUsers` | GET | `/users` | Collection |
|
|
162
|
+
| `createUser` | POST | `/users` | Create resource |
|
|
163
|
+
|
|
164
|
+
### v1.1+ - Full REST Support
|
|
165
|
+
|
|
166
|
+
| Procedure Name | HTTP Method | REST Path |
|
|
167
|
+
|---------------|-------------|-----------|
|
|
168
|
+
| `updateUser` | PUT | `/users/:id` |
|
|
169
|
+
| `patchUser` | PATCH | `/users/:id` |
|
|
170
|
+
| `deleteUser` | DELETE | `/users/:id` |
|
|
171
|
+
|
|
172
|
+
### Custom REST Paths
|
|
173
|
+
|
|
174
|
+
Override naming conventions when needed:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const userProcedures = defineProcedures('users', {
|
|
178
|
+
// Custom path for search endpoint
|
|
179
|
+
searchUsers: procedure()
|
|
180
|
+
.rest({ method: 'GET', path: '/users/search' })
|
|
181
|
+
.input(z.object({ q: z.string() }))
|
|
182
|
+
.output(z.array(UserSchema))
|
|
183
|
+
.query(async ({ input, ctx }) => {
|
|
184
|
+
return ctx.db.user.findMany({
|
|
185
|
+
where: {
|
|
186
|
+
OR: [
|
|
187
|
+
{ name: { contains: input.q } },
|
|
188
|
+
{ email: { contains: input.q } },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
// Custom path for password reset
|
|
195
|
+
resetPassword: procedure()
|
|
196
|
+
.rest({ method: 'POST', path: '/auth/password-reset' })
|
|
197
|
+
.input(z.object({ email: z.string().email() }))
|
|
198
|
+
.output(z.object({ success: z.boolean() }))
|
|
199
|
+
.mutation(async ({ input, ctx }) => {
|
|
200
|
+
// Send password reset email
|
|
201
|
+
return { success: true };
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Registering Routes
|
|
207
|
+
|
|
208
|
+
### REST Adapter
|
|
209
|
+
|
|
210
|
+
Register REST routes with your VeloxTS app:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { createVeloxApp } from '@veloxts/core';
|
|
214
|
+
import { registerRestRoutes } from '@veloxts/router';
|
|
215
|
+
import { userProcedures } from './procedures/users';
|
|
216
|
+
|
|
217
|
+
const app = await createVeloxApp({ port: 3000 });
|
|
218
|
+
|
|
219
|
+
// Register REST routes
|
|
220
|
+
await registerRestRoutes(app.server, {
|
|
221
|
+
prefix: '/api',
|
|
222
|
+
procedures: {
|
|
223
|
+
users: userProcedures,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await app.start();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This generates the following endpoints:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
GET /api/users/:id (getUser)
|
|
234
|
+
GET /api/users (listUsers)
|
|
235
|
+
POST /api/users (createUser)
|
|
236
|
+
GET /api/users/search (searchUsers)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### tRPC Adapter
|
|
240
|
+
|
|
241
|
+
For type-safe internal API calls from your frontend:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { createAppRouter, registerTRPCPlugin } from '@veloxts/router';
|
|
245
|
+
import { userProcedures, postProcedures } from './procedures';
|
|
246
|
+
|
|
247
|
+
// Create tRPC router
|
|
248
|
+
const appRouter = createAppRouter({
|
|
249
|
+
users: userProcedures,
|
|
250
|
+
posts: postProcedures,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Register tRPC plugin
|
|
254
|
+
await registerTRPCPlugin(app.server, {
|
|
255
|
+
router: appRouter,
|
|
256
|
+
prefix: '/trpc',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Export type for frontend
|
|
260
|
+
export type AppRouter = typeof appRouter;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Frontend usage:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { createClient } from '@veloxts/client';
|
|
267
|
+
import type { AppRouter } from '../server';
|
|
268
|
+
|
|
269
|
+
const api = createClient<AppRouter>({ baseUrl: '/api' });
|
|
270
|
+
|
|
271
|
+
// Fully typed calls
|
|
272
|
+
const user = await api.users.getUser({ id: '123' });
|
|
273
|
+
// ^? User (inferred from UserSchema)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Middleware
|
|
277
|
+
|
|
278
|
+
Add cross-cutting concerns with middleware:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const loggerMiddleware: MiddlewareFunction = async ({ ctx, next }) => {
|
|
282
|
+
const start = Date.now();
|
|
283
|
+
console.log(`[${ctx.request.method}] ${ctx.request.url}`);
|
|
284
|
+
|
|
285
|
+
const result = await next();
|
|
286
|
+
|
|
287
|
+
const duration = Date.now() - start;
|
|
288
|
+
console.log(`[${ctx.request.method}] ${ctx.request.url} - ${duration}ms`);
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const userProcedures = defineProcedures('users', {
|
|
294
|
+
getUser: procedure()
|
|
295
|
+
.use(loggerMiddleware)
|
|
296
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
297
|
+
.output(UserSchema)
|
|
298
|
+
.query(async ({ input, ctx }) => {
|
|
299
|
+
return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Type Inference
|
|
305
|
+
|
|
306
|
+
VeloxTS automatically infers types throughout the procedure chain:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const userProcedures = defineProcedures('users', {
|
|
310
|
+
getUser: procedure()
|
|
311
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
312
|
+
.output(UserSchema)
|
|
313
|
+
.query(async ({ input, ctx }) => {
|
|
314
|
+
// input is typed as { id: string }
|
|
315
|
+
// ^? { id: string }
|
|
316
|
+
|
|
317
|
+
// ctx is typed as BaseContext (with extensions)
|
|
318
|
+
// ^? BaseContext & { db: PrismaClient }
|
|
319
|
+
|
|
320
|
+
// Return type must match UserSchema
|
|
321
|
+
return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
322
|
+
// ^? User | null
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Frontend infers the complete type
|
|
327
|
+
const user = await api.users.getUser({ id: '123' });
|
|
328
|
+
// ^? User | null
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Type Helpers
|
|
332
|
+
|
|
333
|
+
Extract types from procedures:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import type {
|
|
337
|
+
InferProcedureInput,
|
|
338
|
+
InferProcedureOutput,
|
|
339
|
+
InferProcedures,
|
|
340
|
+
} from '@veloxts/router';
|
|
341
|
+
|
|
342
|
+
// Infer input type
|
|
343
|
+
type GetUserInput = InferProcedureInput<typeof userProcedures.getUser>;
|
|
344
|
+
// { id: string }
|
|
345
|
+
|
|
346
|
+
// Infer output type
|
|
347
|
+
type GetUserOutput = InferProcedureOutput<typeof userProcedures.getUser>;
|
|
348
|
+
// User | null
|
|
349
|
+
|
|
350
|
+
// Infer all procedures
|
|
351
|
+
type UserProcedures = InferProcedures<typeof userProcedures>;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Context System
|
|
355
|
+
|
|
356
|
+
Procedures receive a context object with request-scoped state:
|
|
357
|
+
|
|
358
|
+
### Base Context
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
interface BaseContext {
|
|
362
|
+
request: FastifyRequest;
|
|
363
|
+
reply: FastifyReply;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Extending Context
|
|
368
|
+
|
|
369
|
+
Plugins can extend context via declaration merging:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import type { PrismaClient } from '@prisma/client';
|
|
373
|
+
|
|
374
|
+
declare module '@veloxts/core' {
|
|
375
|
+
interface BaseContext {
|
|
376
|
+
db: PrismaClient; // Added by @veloxts/orm
|
|
377
|
+
user?: User; // Added by @veloxts/auth (v1.1+)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Now all procedures have ctx.db and ctx.user
|
|
382
|
+
const userProcedures = defineProcedures('users', {
|
|
383
|
+
getProfile: procedure()
|
|
384
|
+
.output(UserSchema)
|
|
385
|
+
.query(async ({ ctx }) => {
|
|
386
|
+
// ctx.db is available (typed as PrismaClient)
|
|
387
|
+
// ctx.user is available (typed as User | undefined)
|
|
388
|
+
if (!ctx.user) {
|
|
389
|
+
throw new UnauthorizedError('Must be logged in');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return ctx.db.user.findUnique({
|
|
393
|
+
where: { id: ctx.user.id },
|
|
394
|
+
});
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Practical Examples
|
|
400
|
+
|
|
401
|
+
### Complete CRUD API
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
405
|
+
import { z, paginationInputSchema } from '@veloxts/validation';
|
|
406
|
+
import { NotFoundError } from '@veloxts/core';
|
|
407
|
+
|
|
408
|
+
const UserSchema = z.object({
|
|
409
|
+
id: z.string().uuid(),
|
|
410
|
+
name: z.string(),
|
|
411
|
+
email: z.string().email(),
|
|
412
|
+
createdAt: z.string().datetime(),
|
|
413
|
+
updatedAt: z.string().datetime(),
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const CreateUserInput = z.object({
|
|
417
|
+
name: z.string().min(1).max(100),
|
|
418
|
+
email: z.string().email(),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
export const userProcedures = defineProcedures('users', {
|
|
422
|
+
// GET /users/:id
|
|
423
|
+
getUser: procedure()
|
|
424
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
425
|
+
.output(UserSchema.nullable())
|
|
426
|
+
.query(async ({ input, ctx }) => {
|
|
427
|
+
return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
428
|
+
}),
|
|
429
|
+
|
|
430
|
+
// GET /users
|
|
431
|
+
listUsers: procedure()
|
|
432
|
+
.input(paginationInputSchema.optional())
|
|
433
|
+
.output(z.object({
|
|
434
|
+
data: z.array(UserSchema),
|
|
435
|
+
meta: z.object({
|
|
436
|
+
page: z.number(),
|
|
437
|
+
limit: z.number(),
|
|
438
|
+
total: z.number(),
|
|
439
|
+
}),
|
|
440
|
+
}))
|
|
441
|
+
.query(async ({ input, ctx }) => {
|
|
442
|
+
const page = input?.page ?? 1;
|
|
443
|
+
const limit = input?.limit ?? 10;
|
|
444
|
+
const skip = (page - 1) * limit;
|
|
445
|
+
|
|
446
|
+
const [data, total] = await Promise.all([
|
|
447
|
+
ctx.db.user.findMany({ skip, take: limit }),
|
|
448
|
+
ctx.db.user.count(),
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
return { data, meta: { page, limit, total } };
|
|
452
|
+
}),
|
|
453
|
+
|
|
454
|
+
// POST /users
|
|
455
|
+
createUser: procedure()
|
|
456
|
+
.input(CreateUserInput)
|
|
457
|
+
.output(UserSchema)
|
|
458
|
+
.mutation(async ({ input, ctx }) => {
|
|
459
|
+
return ctx.db.user.create({ data: input });
|
|
460
|
+
}),
|
|
461
|
+
|
|
462
|
+
// PUT /users/:id (v1.1+)
|
|
463
|
+
// DELETE /users/:id (v1.1+)
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Search with Custom Path
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
const postProcedures = defineProcedures('posts', {
|
|
471
|
+
// GET /posts/search?q=keyword&published=true
|
|
472
|
+
searchPosts: procedure()
|
|
473
|
+
.rest({ method: 'GET', path: '/posts/search' })
|
|
474
|
+
.input(z.object({
|
|
475
|
+
q: z.string().min(1),
|
|
476
|
+
published: z.boolean().optional(),
|
|
477
|
+
}))
|
|
478
|
+
.output(z.array(PostSchema))
|
|
479
|
+
.query(async ({ input, ctx }) => {
|
|
480
|
+
return ctx.db.post.findMany({
|
|
481
|
+
where: {
|
|
482
|
+
AND: [
|
|
483
|
+
{
|
|
484
|
+
OR: [
|
|
485
|
+
{ title: { contains: input.q } },
|
|
486
|
+
{ content: { contains: input.q } },
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
input.published !== undefined
|
|
490
|
+
? { published: input.published }
|
|
491
|
+
: {},
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}),
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Authentication Middleware
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
const authMiddleware: MiddlewareFunction = async ({ ctx, next }) => {
|
|
503
|
+
const token = ctx.request.headers.authorization?.replace('Bearer ', '');
|
|
504
|
+
|
|
505
|
+
if (!token) {
|
|
506
|
+
throw new UnauthorizedError('Missing authentication token');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Verify token and attach user to context
|
|
510
|
+
const user = await verifyToken(token);
|
|
511
|
+
(ctx as ExtendedContext).user = user;
|
|
512
|
+
|
|
513
|
+
return next();
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const protectedProcedures = defineProcedures('protected', {
|
|
517
|
+
getProfile: procedure()
|
|
518
|
+
.use(authMiddleware)
|
|
519
|
+
.output(UserSchema)
|
|
520
|
+
.query(async ({ ctx }) => {
|
|
521
|
+
// ctx.user is guaranteed to exist after authMiddleware
|
|
522
|
+
const user = (ctx as ExtendedContext).user;
|
|
523
|
+
return ctx.db.user.findUnique({ where: { id: user.id } });
|
|
524
|
+
}),
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Error Handling
|
|
529
|
+
|
|
530
|
+
Procedures integrate with VeloxTS error classes:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import {
|
|
534
|
+
VeloxError,
|
|
535
|
+
NotFoundError,
|
|
536
|
+
ValidationError,
|
|
537
|
+
UnauthorizedError,
|
|
538
|
+
} from '@veloxts/core';
|
|
539
|
+
|
|
540
|
+
const userProcedures = defineProcedures('users', {
|
|
541
|
+
getUser: procedure()
|
|
542
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
543
|
+
.output(UserSchema)
|
|
544
|
+
.query(async ({ input, ctx }) => {
|
|
545
|
+
const user = await ctx.db.user.findUnique({
|
|
546
|
+
where: { id: input.id },
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (!user) {
|
|
550
|
+
// Returns 404 with structured error
|
|
551
|
+
throw new NotFoundError('User', input.id);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return user;
|
|
555
|
+
}),
|
|
556
|
+
|
|
557
|
+
deleteUser: procedure()
|
|
558
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
559
|
+
.output(z.object({ success: z.boolean() }))
|
|
560
|
+
.mutation(async ({ input, ctx }) => {
|
|
561
|
+
try {
|
|
562
|
+
await ctx.db.user.delete({ where: { id: input.id } });
|
|
563
|
+
return { success: true };
|
|
564
|
+
} catch (error) {
|
|
565
|
+
// Handle Prisma errors
|
|
566
|
+
if (error.code === 'P2025') {
|
|
567
|
+
throw new NotFoundError('User', input.id);
|
|
568
|
+
}
|
|
569
|
+
throw new VeloxError('Failed to delete user', 500);
|
|
570
|
+
}
|
|
571
|
+
}),
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Configuration
|
|
576
|
+
|
|
577
|
+
### Route Summary
|
|
578
|
+
|
|
579
|
+
Get a summary of generated routes:
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import { getRouteSummary } from '@veloxts/router';
|
|
583
|
+
|
|
584
|
+
const summary = getRouteSummary({
|
|
585
|
+
users: userProcedures,
|
|
586
|
+
posts: postProcedures,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
console.log(summary);
|
|
590
|
+
// [
|
|
591
|
+
// { method: 'GET', path: '/users/:id', procedure: 'users.getUser' },
|
|
592
|
+
// { method: 'GET', path: '/users', procedure: 'users.listUsers' },
|
|
593
|
+
// { method: 'POST', path: '/users', procedure: 'users.createUser' },
|
|
594
|
+
// { method: 'GET', path: '/posts/:id', procedure: 'posts.getPost' },
|
|
595
|
+
// ...
|
|
596
|
+
// ]
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Route Prefix
|
|
600
|
+
|
|
601
|
+
Apply a common prefix to all routes:
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
await registerRestRoutes(app.server, {
|
|
605
|
+
prefix: '/api/v1', // All routes start with /api/v1
|
|
606
|
+
procedures: {
|
|
607
|
+
users: userProcedures,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Generates: GET /api/v1/users/:id
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## MVP Limitations
|
|
615
|
+
|
|
616
|
+
The current v0.1.0 release supports:
|
|
617
|
+
|
|
618
|
+
**Included:**
|
|
619
|
+
- Query procedures (GET)
|
|
620
|
+
- Mutation procedures (POST)
|
|
621
|
+
- Input/output validation with Zod
|
|
622
|
+
- Naming convention-based REST mapping
|
|
623
|
+
- Custom REST path overrides
|
|
624
|
+
- tRPC adapter for type-safe internal calls
|
|
625
|
+
- Middleware support
|
|
626
|
+
|
|
627
|
+
**Deferred to v1.1+:**
|
|
628
|
+
- Full REST verbs (PUT, PATCH, DELETE)
|
|
629
|
+
- Nested resource routing
|
|
630
|
+
- OpenAPI/Swagger documentation generation
|
|
631
|
+
- Rate limiting middleware
|
|
632
|
+
- Request/response transformation hooks
|
|
633
|
+
|
|
634
|
+
## Related Packages
|
|
635
|
+
|
|
636
|
+
- [@veloxts/core](/packages/core) - Core framework with context and errors
|
|
637
|
+
- [@veloxts/validation](/packages/validation) - Zod schemas for input/output
|
|
638
|
+
- [@veloxts/orm](/packages/orm) - Database integration (provides ctx.db)
|
|
639
|
+
- [@veloxts/client](/packages/client) - Type-safe frontend API client
|
|
640
|
+
|
|
641
|
+
## TypeScript Support
|
|
642
|
+
|
|
643
|
+
All exports are fully typed with comprehensive JSDoc documentation. The package includes type definitions and declaration maps for excellent IDE support.
|
|
644
|
+
|
|
645
|
+
## License
|
|
646
|
+
|
|
647
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @veloxts/router - Procedure API with hybrid tRPC/REST routing
|
|
3
|
+
*
|
|
4
|
+
* Core routing abstraction that enables type-safe API endpoints with
|
|
5
|
+
* automatic tRPC and REST adapter generation from procedure definitions.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { procedure, defineProcedures } from '@veloxts/router';
|
|
10
|
+
* import { z } from '@veloxts/validation';
|
|
11
|
+
*
|
|
12
|
+
* const UserSchema = z.object({
|
|
13
|
+
* id: z.string().uuid(),
|
|
14
|
+
* name: z.string(),
|
|
15
|
+
* email: z.string().email(),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* export const userProcedures = defineProcedures('users', {
|
|
19
|
+
* getUser: procedure()
|
|
20
|
+
* .input(z.object({ id: z.string().uuid() }))
|
|
21
|
+
* .output(UserSchema)
|
|
22
|
+
* .query(async ({ input, ctx }) => {
|
|
23
|
+
* // input is typed as { id: string }
|
|
24
|
+
* // ctx is typed as BaseContext
|
|
25
|
+
* // return type must match UserSchema
|
|
26
|
+
* return ctx.db.user.findUnique({ where: { id: input.id } });
|
|
27
|
+
* }),
|
|
28
|
+
*
|
|
29
|
+
* createUser: procedure()
|
|
30
|
+
* .input(z.object({ name: z.string(), email: z.string().email() }))
|
|
31
|
+
* .output(UserSchema)
|
|
32
|
+
* .mutation(async ({ input, ctx }) => {
|
|
33
|
+
* return ctx.db.user.create({ data: input });
|
|
34
|
+
* }),
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @module @veloxts/router
|
|
39
|
+
*/
|
|
40
|
+
export declare const ROUTER_VERSION: "0.1.0";
|
|
41
|
+
export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, InferProcedureTypes, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
|
|
42
|
+
export { PROCEDURE_METHOD_MAP, } from './types.js';
|
|
43
|
+
export { defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, } from './procedure/builder.js';
|
|
44
|
+
export type { BuilderRuntimeState, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidSchema, } from './procedure/types.js';
|
|
45
|
+
export type { RestAdapterOptions, RestMapping, RestRoute } from './rest/index.js';
|
|
46
|
+
export { buildRestPath, createRoutesRegistrar, followsNamingConvention, generateRestRoutes, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, } from './rest/index.js';
|
|
47
|
+
export type { AnyRouter, InferAppRouter, TRPCInstance, TRPCPluginOptions, } from './trpc/index.js';
|
|
48
|
+
export { buildTRPCRouter, createAppRouter, createTRPC, createTRPCContextFactory, registerTRPCPlugin, veloxErrorToTRPCError, } from './trpc/index.js';
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|