@wordrhyme/auto-crud-server 1.0.4 → 1.0.6
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/README.md +373 -175
- package/dist/index.cjs +116 -44
- package/dist/index.d.cts +207 -95
- package/dist/index.d.ts +206 -94
- package/dist/index.js +114 -45
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,20 +52,24 @@ yarn add @wordrhyme/auto-crud-server
|
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
54
|
// src/db/schema.ts
|
|
55
|
-
import { pgTable, varchar, real, boolean, timestamp } from
|
|
55
|
+
import { pgTable, varchar, real, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
56
56
|
|
|
57
|
-
export const tasks = pgTable(
|
|
58
|
-
id: varchar(
|
|
59
|
-
title: varchar(
|
|
60
|
-
status: varchar(
|
|
61
|
-
enum: [
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
export const tasks = pgTable('tasks', {
|
|
58
|
+
id: varchar('id', { length: 30 }).primaryKey(),
|
|
59
|
+
title: varchar('title', { length: 128 }).notNull(),
|
|
60
|
+
status: varchar('status', {
|
|
61
|
+
enum: ['todo', 'in-progress', 'done', 'canceled'],
|
|
62
|
+
})
|
|
63
|
+
.notNull()
|
|
64
|
+
.default('todo'),
|
|
65
|
+
priority: varchar('priority', {
|
|
66
|
+
enum: ['low', 'medium', 'high'],
|
|
67
|
+
})
|
|
68
|
+
.notNull()
|
|
69
|
+
.default('low'),
|
|
70
|
+
estimatedHours: real('estimated_hours').default(0),
|
|
71
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
72
|
+
updatedAt: timestamp('updated_at').defaultNow(),
|
|
69
73
|
});
|
|
70
74
|
```
|
|
71
75
|
|
|
@@ -73,8 +77,8 @@ export const tasks = pgTable("tasks", {
|
|
|
73
77
|
|
|
74
78
|
```typescript
|
|
75
79
|
// src/server/routers/tasks.ts
|
|
76
|
-
import { createCrudRouter } from
|
|
77
|
-
import { tasks } from
|
|
80
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
81
|
+
import { tasks } from '@/db/schema';
|
|
78
82
|
|
|
79
83
|
// 🚀 零配置!一行代码生成完整 CRUD 路由
|
|
80
84
|
export const tasksRouter = createCrudRouter({
|
|
@@ -86,9 +90,9 @@ export const tasksRouter = createCrudRouter({
|
|
|
86
90
|
**或者,显式传入 Schema:**
|
|
87
91
|
|
|
88
92
|
```typescript
|
|
89
|
-
import { createCrudRouter } from
|
|
90
|
-
import { tasks } from
|
|
91
|
-
import { createSelectSchema } from
|
|
93
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
94
|
+
import { tasks } from '@/db/schema';
|
|
95
|
+
import { createSelectSchema } from 'drizzle-zod';
|
|
92
96
|
|
|
93
97
|
// 从 Drizzle Schema 自动生成 Zod Schema
|
|
94
98
|
const taskSchema = createSelectSchema(tasks).omit({
|
|
@@ -99,7 +103,7 @@ const taskSchema = createSelectSchema(tasks).omit({
|
|
|
99
103
|
|
|
100
104
|
export const tasksRouter = createCrudRouter({
|
|
101
105
|
table: tasks,
|
|
102
|
-
schema: taskSchema,
|
|
106
|
+
schema: taskSchema, // 主 Schema(用于 create/upsert)
|
|
103
107
|
// updateSchema 自动派生为 schema.partial()
|
|
104
108
|
});
|
|
105
109
|
```
|
|
@@ -108,8 +112,8 @@ export const tasksRouter = createCrudRouter({
|
|
|
108
112
|
|
|
109
113
|
```typescript
|
|
110
114
|
// src/server/routers/index.ts
|
|
111
|
-
import { router } from
|
|
112
|
-
import { tasksRouter } from
|
|
115
|
+
import { router } from '../trpc';
|
|
116
|
+
import { tasksRouter } from './tasks';
|
|
113
117
|
|
|
114
118
|
export const appRouter = router({
|
|
115
119
|
tasks: tasksRouter,
|
|
@@ -122,13 +126,13 @@ export type AppRouter = typeof appRouter;
|
|
|
122
126
|
|
|
123
127
|
```typescript
|
|
124
128
|
// src/app/api/trpc/[trpc]/route.ts (Next.js App Router)
|
|
125
|
-
import { fetchRequestHandler } from
|
|
126
|
-
import { appRouter } from
|
|
127
|
-
import { db } from
|
|
129
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
130
|
+
import { appRouter } from '@/server/routers';
|
|
131
|
+
import { db } from '@/db';
|
|
128
132
|
|
|
129
133
|
const handler = (req: Request) =>
|
|
130
134
|
fetchRequestHandler({
|
|
131
|
-
endpoint:
|
|
135
|
+
endpoint: '/api/trpc',
|
|
132
136
|
req,
|
|
133
137
|
router: appRouter,
|
|
134
138
|
createContext: () => ({ db }),
|
|
@@ -146,6 +150,7 @@ export { handler as GET, handler as POST };
|
|
|
146
150
|
### 1. `list` - 列表查询
|
|
147
151
|
|
|
148
152
|
**输入**:
|
|
153
|
+
|
|
149
154
|
```typescript
|
|
150
155
|
{
|
|
151
156
|
page: number; // 页码(从 1 开始)
|
|
@@ -165,6 +170,7 @@ export { handler as GET, handler as POST };
|
|
|
165
170
|
```
|
|
166
171
|
|
|
167
172
|
**输出**:
|
|
173
|
+
|
|
168
174
|
```typescript
|
|
169
175
|
{
|
|
170
176
|
data: Task[]; // 数据列表
|
|
@@ -173,117 +179,138 @@ export { handler as GET, handler as POST };
|
|
|
173
179
|
```
|
|
174
180
|
|
|
175
181
|
**示例**:
|
|
182
|
+
|
|
176
183
|
```typescript
|
|
177
184
|
const result = await trpc.tasks.list({
|
|
178
185
|
page: 1,
|
|
179
186
|
perPage: 10,
|
|
180
|
-
sort: [{ id:
|
|
187
|
+
sort: [{ id: 'createdAt', desc: true }],
|
|
181
188
|
filters: [
|
|
182
|
-
{ id:
|
|
183
|
-
{ id:
|
|
189
|
+
{ id: 'status', value: 'done', operator: 'eq', variant: 'select' },
|
|
190
|
+
{ id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
|
|
184
191
|
],
|
|
185
|
-
joinOperator:
|
|
192
|
+
joinOperator: 'and',
|
|
186
193
|
});
|
|
187
194
|
```
|
|
188
195
|
|
|
189
196
|
### 2. `get` - 单条查询
|
|
190
197
|
|
|
191
198
|
**输入**:
|
|
199
|
+
|
|
192
200
|
```typescript
|
|
193
|
-
{
|
|
201
|
+
{
|
|
202
|
+
id: string;
|
|
203
|
+
}
|
|
194
204
|
```
|
|
195
205
|
|
|
196
206
|
**输出**:
|
|
207
|
+
|
|
197
208
|
```typescript
|
|
198
|
-
Task
|
|
209
|
+
Task;
|
|
199
210
|
```
|
|
200
211
|
|
|
201
212
|
**示例**:
|
|
213
|
+
|
|
202
214
|
```typescript
|
|
203
|
-
const task = await trpc.tasks.get({ id:
|
|
215
|
+
const task = await trpc.tasks.get({ id: '123' });
|
|
204
216
|
```
|
|
205
217
|
|
|
206
218
|
### 3. `create` - 创建
|
|
207
219
|
|
|
208
220
|
**输入**:
|
|
221
|
+
|
|
209
222
|
```typescript
|
|
210
|
-
Omit<Task,
|
|
223
|
+
Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;
|
|
211
224
|
```
|
|
212
225
|
|
|
213
226
|
**输出**:
|
|
227
|
+
|
|
214
228
|
```typescript
|
|
215
|
-
Task
|
|
229
|
+
Task;
|
|
216
230
|
```
|
|
217
231
|
|
|
218
232
|
**示例**:
|
|
233
|
+
|
|
219
234
|
```typescript
|
|
220
235
|
const newTask = await trpc.tasks.create({
|
|
221
|
-
title:
|
|
222
|
-
status:
|
|
223
|
-
priority:
|
|
236
|
+
title: 'New Task',
|
|
237
|
+
status: 'todo',
|
|
238
|
+
priority: 'high',
|
|
224
239
|
});
|
|
225
240
|
```
|
|
226
241
|
|
|
227
242
|
### 4. `update` - 更新
|
|
228
243
|
|
|
229
244
|
**输入**:
|
|
245
|
+
|
|
230
246
|
```typescript
|
|
231
247
|
{
|
|
232
248
|
id: string;
|
|
233
|
-
data: Partial<Omit<Task,
|
|
249
|
+
data: Partial<Omit<Task, 'id' | 'createdAt' | 'updatedAt'>>;
|
|
234
250
|
}
|
|
235
251
|
```
|
|
236
252
|
|
|
237
253
|
**输出**:
|
|
254
|
+
|
|
238
255
|
```typescript
|
|
239
|
-
Task
|
|
256
|
+
Task;
|
|
240
257
|
```
|
|
241
258
|
|
|
242
259
|
**示例**:
|
|
260
|
+
|
|
243
261
|
```typescript
|
|
244
262
|
const updatedTask = await trpc.tasks.update({
|
|
245
|
-
id:
|
|
246
|
-
data: { status:
|
|
263
|
+
id: '123',
|
|
264
|
+
data: { status: 'done' },
|
|
247
265
|
});
|
|
248
266
|
```
|
|
249
267
|
|
|
250
268
|
### 5. `delete` - 删除
|
|
251
269
|
|
|
252
270
|
**输入**:
|
|
271
|
+
|
|
253
272
|
```typescript
|
|
254
|
-
{
|
|
273
|
+
{
|
|
274
|
+
id: string;
|
|
275
|
+
}
|
|
255
276
|
```
|
|
256
277
|
|
|
257
278
|
**输出**:
|
|
279
|
+
|
|
258
280
|
```typescript
|
|
259
281
|
void
|
|
260
282
|
```
|
|
261
283
|
|
|
262
284
|
**示例**:
|
|
285
|
+
|
|
263
286
|
```typescript
|
|
264
|
-
await trpc.tasks.delete({ id:
|
|
287
|
+
await trpc.tasks.delete({ id: '123' });
|
|
265
288
|
```
|
|
266
289
|
|
|
267
290
|
### 6. `deleteMany` - 批量删除
|
|
268
291
|
|
|
269
292
|
**输入**:
|
|
293
|
+
|
|
270
294
|
```typescript
|
|
271
295
|
{ ids: string[] }
|
|
272
296
|
```
|
|
273
297
|
|
|
274
298
|
**输出**:
|
|
299
|
+
|
|
275
300
|
```typescript
|
|
276
301
|
void
|
|
277
302
|
```
|
|
278
303
|
|
|
279
304
|
**示例**:
|
|
305
|
+
|
|
280
306
|
```typescript
|
|
281
|
-
await trpc.tasks.deleteMany({ ids: [
|
|
307
|
+
await trpc.tasks.deleteMany({ ids: ['1', '2', '3'] });
|
|
282
308
|
```
|
|
283
309
|
|
|
284
310
|
### 7. `updateMany` - 批量更新
|
|
285
311
|
|
|
286
312
|
**输入**:
|
|
313
|
+
|
|
287
314
|
```typescript
|
|
288
315
|
{
|
|
289
316
|
ids: string[];
|
|
@@ -292,50 +319,58 @@ await trpc.tasks.deleteMany({ ids: ["1", "2", "3"] });
|
|
|
292
319
|
```
|
|
293
320
|
|
|
294
321
|
**输出**:
|
|
322
|
+
|
|
295
323
|
```typescript
|
|
296
|
-
{
|
|
324
|
+
{
|
|
325
|
+
updated: number;
|
|
326
|
+
}
|
|
297
327
|
```
|
|
298
328
|
|
|
299
329
|
**示例**:
|
|
330
|
+
|
|
300
331
|
```typescript
|
|
301
332
|
await trpc.tasks.updateMany({
|
|
302
|
-
ids: [
|
|
303
|
-
data: { status:
|
|
333
|
+
ids: ['1', '2', '3'],
|
|
334
|
+
data: { status: 'done' },
|
|
304
335
|
});
|
|
305
336
|
```
|
|
306
337
|
|
|
307
338
|
### 8. `upsert` - 存在则更新,不存在则创建
|
|
308
339
|
|
|
309
340
|
**输入**:
|
|
341
|
+
|
|
310
342
|
```typescript
|
|
311
|
-
Omit<Task,
|
|
343
|
+
Omit<Task, 'createdAt' | 'updatedAt'>; // 需要包含 id
|
|
312
344
|
```
|
|
313
345
|
|
|
314
346
|
**输出**:
|
|
347
|
+
|
|
315
348
|
```typescript
|
|
316
349
|
{
|
|
317
|
-
data: Task;
|
|
318
|
-
isNew: boolean;
|
|
350
|
+
data: Task; // 创建或更新后的记录
|
|
351
|
+
isNew: boolean; // true = 新建, false = 更新
|
|
319
352
|
}
|
|
320
353
|
```
|
|
321
354
|
|
|
322
355
|
**示例**:
|
|
356
|
+
|
|
323
357
|
```typescript
|
|
324
358
|
// 如果 id="123" 存在则更新,不存在则创建
|
|
325
359
|
const { data, isNew } = await trpc.tasks.upsert({
|
|
326
|
-
id:
|
|
327
|
-
title:
|
|
328
|
-
status:
|
|
360
|
+
id: '123',
|
|
361
|
+
title: 'My Task',
|
|
362
|
+
status: 'todo',
|
|
329
363
|
});
|
|
330
364
|
|
|
331
365
|
if (isNew) {
|
|
332
|
-
console.log(
|
|
366
|
+
console.log('Created new task');
|
|
333
367
|
} else {
|
|
334
|
-
console.log(
|
|
368
|
+
console.log('Updated existing task');
|
|
335
369
|
}
|
|
336
370
|
```
|
|
337
371
|
|
|
338
372
|
**适用场景**:
|
|
373
|
+
|
|
339
374
|
- 同步外部数据
|
|
340
375
|
- 幂等导入
|
|
341
376
|
- 配置项更新
|
|
@@ -346,21 +381,21 @@ if (isNew) {
|
|
|
346
381
|
|
|
347
382
|
### 支持的操作符
|
|
348
383
|
|
|
349
|
-
| 操作符
|
|
350
|
-
|
|
351
|
-
| `eq`
|
|
352
|
-
| `ne`
|
|
353
|
-
| `gt`
|
|
354
|
-
| `gte`
|
|
355
|
-
| `lt`
|
|
356
|
-
| `lte`
|
|
357
|
-
| `like`
|
|
358
|
-
| `notLike`
|
|
359
|
-
| `in`
|
|
360
|
-
| `notIn`
|
|
361
|
-
| `between`
|
|
362
|
-
| `isNull`
|
|
363
|
-
| `isNotNull` | 不为空
|
|
384
|
+
| 操作符 | 说明 | 示例 |
|
|
385
|
+
| ----------- | ---------- | ------------------------------------------------- |
|
|
386
|
+
| `eq` | 等于 | `status = "done"` |
|
|
387
|
+
| `ne` | 不等于 | `status != "canceled"` |
|
|
388
|
+
| `gt` | 大于 | `estimatedHours > 5` |
|
|
389
|
+
| `gte` | 大于等于 | `estimatedHours >= 5` |
|
|
390
|
+
| `lt` | 小于 | `estimatedHours < 10` |
|
|
391
|
+
| `lte` | 小于等于 | `estimatedHours <= 10` |
|
|
392
|
+
| `like` | 包含 | `title LIKE "%bug%"` |
|
|
393
|
+
| `notLike` | 不包含 | `title NOT LIKE "%test%"` |
|
|
394
|
+
| `in` | 在列表中 | `status IN ["todo", "in-progress"]` |
|
|
395
|
+
| `notIn` | 不在列表中 | `status NOT IN ["canceled"]` |
|
|
396
|
+
| `between` | 范围 | `createdAt BETWEEN "2024-01-01" AND "2024-12-31"` |
|
|
397
|
+
| `isNull` | 为空 | `description IS NULL` |
|
|
398
|
+
| `isNotNull` | 不为空 | `description IS NOT NULL` |
|
|
364
399
|
|
|
365
400
|
### 过滤示例
|
|
366
401
|
|
|
@@ -370,9 +405,7 @@ if (isNew) {
|
|
|
370
405
|
await trpc.tasks.list({
|
|
371
406
|
page: 1,
|
|
372
407
|
perPage: 10,
|
|
373
|
-
filters: [
|
|
374
|
-
{ id: "status", value: "done", operator: "eq", variant: "select" },
|
|
375
|
-
],
|
|
408
|
+
filters: [{ id: 'status', value: 'done', operator: 'eq', variant: 'select' }],
|
|
376
409
|
});
|
|
377
410
|
```
|
|
378
411
|
|
|
@@ -383,10 +416,10 @@ await trpc.tasks.list({
|
|
|
383
416
|
page: 1,
|
|
384
417
|
perPage: 10,
|
|
385
418
|
filters: [
|
|
386
|
-
{ id:
|
|
387
|
-
{ id:
|
|
419
|
+
{ id: 'status', value: 'done', operator: 'eq', variant: 'select' },
|
|
420
|
+
{ id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
|
|
388
421
|
],
|
|
389
|
-
joinOperator:
|
|
422
|
+
joinOperator: 'and', // status = "done" AND priority = "high"
|
|
390
423
|
});
|
|
391
424
|
```
|
|
392
425
|
|
|
@@ -397,10 +430,10 @@ await trpc.tasks.list({
|
|
|
397
430
|
page: 1,
|
|
398
431
|
perPage: 10,
|
|
399
432
|
filters: [
|
|
400
|
-
{ id:
|
|
401
|
-
{ id:
|
|
433
|
+
{ id: 'status', value: 'todo', operator: 'eq', variant: 'select' },
|
|
434
|
+
{ id: 'status', value: 'in-progress', operator: 'eq', variant: 'select' },
|
|
402
435
|
],
|
|
403
|
-
joinOperator:
|
|
436
|
+
joinOperator: 'or', // status = "todo" OR status = "in-progress"
|
|
404
437
|
});
|
|
405
438
|
```
|
|
406
439
|
|
|
@@ -412,10 +445,10 @@ await trpc.tasks.list({
|
|
|
412
445
|
perPage: 10,
|
|
413
446
|
filters: [
|
|
414
447
|
{
|
|
415
|
-
id:
|
|
416
|
-
value: [
|
|
417
|
-
operator:
|
|
418
|
-
variant:
|
|
448
|
+
id: 'createdAt',
|
|
449
|
+
value: ['2024-01-01', '2024-12-31'],
|
|
450
|
+
operator: 'between',
|
|
451
|
+
variant: 'dateRange',
|
|
419
452
|
},
|
|
420
453
|
],
|
|
421
454
|
});
|
|
@@ -427,9 +460,7 @@ await trpc.tasks.list({
|
|
|
427
460
|
await trpc.tasks.list({
|
|
428
461
|
page: 1,
|
|
429
462
|
perPage: 10,
|
|
430
|
-
filters: [
|
|
431
|
-
{ id: "title", value: "bug", operator: "like", variant: "text" },
|
|
432
|
-
],
|
|
463
|
+
filters: [{ id: 'title', value: 'bug', operator: 'like', variant: 'text' }],
|
|
433
464
|
});
|
|
434
465
|
```
|
|
435
466
|
|
|
@@ -446,25 +477,30 @@ await trpc.tasks.list({
|
|
|
446
477
|
```typescript
|
|
447
478
|
interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
|
|
448
479
|
// ========== 必填 ==========
|
|
449
|
-
table: TTable;
|
|
480
|
+
table: TTable; // Drizzle 表定义
|
|
450
481
|
|
|
451
482
|
// ========== Schema 配置(可选) ==========
|
|
452
|
-
schema?: z.ZodType<TInsert>;
|
|
453
|
-
updateSchema?: z.ZodType<TUpdate
|
|
454
|
-
selectSchema?: z.ZodType<TSelect
|
|
483
|
+
schema?: z.ZodType<TInsert>; // 主 Schema(用于 create/upsert)
|
|
484
|
+
updateSchema?: z.ZodType<TUpdate>; // 更新 Schema(覆盖自动派生)
|
|
485
|
+
selectSchema?: z.ZodType<TSelect>; // 查询返回 Schema
|
|
486
|
+
listInputSchema?: z.ZodType; // list 输入 Schema(可扩展)
|
|
487
|
+
getInputSchema?: z.ZodType; // get 输入 Schema(可扩展)
|
|
488
|
+
exportInputSchema?: z.ZodType; // export 输入 Schema(可扩展)
|
|
455
489
|
|
|
456
490
|
// ========== 其他配置 ==========
|
|
457
|
-
idField?: string;
|
|
458
|
-
|
|
459
|
-
|
|
491
|
+
idField?: string; // ID 字段名,默认 "id"
|
|
492
|
+
filterableColumns?: CrudColumnRef<TTable>[]; // 可过滤字段白名单
|
|
493
|
+
sortableColumns?: CrudColumnRef<TTable>[]; // 可排序字段白名单
|
|
494
|
+
omitFields?: string[]; // 自动派生时排除的字段
|
|
495
|
+
// 默认 ["id", "createdAt", "updatedAt"]
|
|
460
496
|
}
|
|
461
497
|
```
|
|
462
498
|
|
|
463
499
|
#### Schema 派生规则
|
|
464
500
|
|
|
465
|
-
| 配置
|
|
466
|
-
|
|
467
|
-
| 无 `schema`
|
|
501
|
+
| 配置 | 派生行为 |
|
|
502
|
+
| ----------------- | ------------------------------------------- |
|
|
503
|
+
| 无 `schema` | 从 `table` 自动派生,排除 `omitFields` |
|
|
468
504
|
| 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
|
|
469
505
|
| 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
|
|
470
506
|
|
|
@@ -492,10 +528,121 @@ const usersRouter = createCrudRouter({
|
|
|
492
528
|
// 4. 自定义排除字段
|
|
493
529
|
const ordersRouter = createCrudRouter({
|
|
494
530
|
table: orders,
|
|
495
|
-
omitFields: [
|
|
531
|
+
omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### 扩展读取输入
|
|
536
|
+
|
|
537
|
+
默认 `list` 输入为 `baseListInputSchema`:
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
{
|
|
541
|
+
page: number;
|
|
542
|
+
perPage: number;
|
|
543
|
+
sort?: Array<{ id: string; desc: boolean }>;
|
|
544
|
+
filters?: Array<FilterItem>;
|
|
545
|
+
joinOperator: "and" | "or";
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
业务侧可以用 `baseListInputSchema.extend(...)` 增加自定义参数,不需要覆盖
|
|
550
|
+
`list` procedure。默认查询逻辑仍只读取分页、排序、过滤和 `joinOperator`,
|
|
551
|
+
额外字段会保留在 `middleware.list` 的 `input` 中。
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { baseListInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
555
|
+
import { z } from 'zod';
|
|
556
|
+
|
|
557
|
+
const productsRouter = createCrudRouter({
|
|
558
|
+
table: shopProducts,
|
|
559
|
+
idField: 'spuId',
|
|
560
|
+
schema: createProductSchema,
|
|
561
|
+
updateSchema: updateProductSchema,
|
|
562
|
+
listInputSchema: baseListInputSchema.extend({
|
|
563
|
+
include: z
|
|
564
|
+
.object({
|
|
565
|
+
skus: z.boolean().optional(),
|
|
566
|
+
})
|
|
567
|
+
.optional(),
|
|
568
|
+
}),
|
|
569
|
+
middleware: {
|
|
570
|
+
list: async ({ ctx, input, next }) => {
|
|
571
|
+
const result = await next(input);
|
|
572
|
+
if (!input.include?.skus) return result;
|
|
573
|
+
return attachSkus(ctx, result);
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// 调用方
|
|
579
|
+
await trpc.products.list.query({
|
|
580
|
+
page: 1,
|
|
581
|
+
perPage: 20,
|
|
582
|
+
include: { skus: true },
|
|
496
583
|
});
|
|
497
584
|
```
|
|
498
585
|
|
|
586
|
+
`get` 也可以扩展。默认仍兼容 `get("spu_1")`,业务侧如需详情页按需挂载关联数据,可以改成对象输入:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
import { baseGetInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
590
|
+
import { z } from 'zod';
|
|
591
|
+
|
|
592
|
+
const productsRouter = createCrudRouter({
|
|
593
|
+
table: shopProducts,
|
|
594
|
+
idField: 'spuId',
|
|
595
|
+
schema: createProductSchema,
|
|
596
|
+
updateSchema: updateProductSchema,
|
|
597
|
+
getInputSchema: baseGetInputSchema.extend({
|
|
598
|
+
include: z.object({ skus: z.boolean().optional() }).optional(),
|
|
599
|
+
}),
|
|
600
|
+
middleware: {
|
|
601
|
+
get: async ({ ctx, input, next }) => {
|
|
602
|
+
const product = await next(input);
|
|
603
|
+
if (!product || typeof input === 'string' || !input.include?.skus) {
|
|
604
|
+
return product;
|
|
605
|
+
}
|
|
606
|
+
return attachProductSkus(ctx, product);
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
await trpc.products.get.query({
|
|
612
|
+
id: 'spu_1',
|
|
613
|
+
include: { skus: true },
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`export` 支持同样的扩展方式,默认导出逻辑只读取 `sort`、`filters`、`joinOperator` 和 `limit`:
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
import { baseExportInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
621
|
+
import { z } from 'zod';
|
|
622
|
+
|
|
623
|
+
const productsRouter = createCrudRouter({
|
|
624
|
+
table: shopProducts,
|
|
625
|
+
schema: createProductSchema,
|
|
626
|
+
updateSchema: updateProductSchema,
|
|
627
|
+
exportInputSchema: baseExportInputSchema.extend({
|
|
628
|
+
format: z.enum(['csv', 'xlsx']).optional(),
|
|
629
|
+
}),
|
|
630
|
+
middleware: {
|
|
631
|
+
export: async ({ input, next }) => {
|
|
632
|
+
const result = await next(input);
|
|
633
|
+
return input.format === 'xlsx' ? toXlsxExport(result) : result;
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
await trpc.products.export.query({
|
|
639
|
+
limit: 1000,
|
|
640
|
+
format: 'xlsx',
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
写入类内置方法(`create`、`update`、`upsert`、`createMany`)不提供通用控制参数扩展;它们的输入字段会进入默认写入逻辑。需要 `dryRun`、`notify` 等控制参数时,建议单独设计 envelope 或自定义业务 procedure。
|
|
645
|
+
|
|
499
646
|
#### 返回值
|
|
500
647
|
|
|
501
648
|
```typescript
|
|
@@ -507,7 +654,7 @@ const ordersRouter = createCrudRouter({
|
|
|
507
654
|
// 可 spread 的 procedures 对象(用于扩展自定义路由)
|
|
508
655
|
procedures: {
|
|
509
656
|
list: Procedure<ListInput, ListOutput>,
|
|
510
|
-
get: Procedure<string, TSelect>,
|
|
657
|
+
get: Procedure<string | { id: string, ...extra }, TSelect>,
|
|
511
658
|
create: Procedure<TInsert, TSelect>,
|
|
512
659
|
update: Procedure<{ id: string, data: TUpdate }, TSelect>,
|
|
513
660
|
delete: Procedure<string, TSelect>,
|
|
@@ -525,18 +672,18 @@ const ordersRouter = createCrudRouter({
|
|
|
525
672
|
### 使用 procedure 配置
|
|
526
673
|
|
|
527
674
|
```typescript
|
|
528
|
-
import { createCrudRouter } from
|
|
529
|
-
import { protectedProcedure, adminProcedure, publicProcedure } from
|
|
675
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
676
|
+
import { protectedProcedure, adminProcedure, publicProcedure } from '../trpc';
|
|
530
677
|
|
|
531
678
|
export const tasksRouter = createCrudRouter({
|
|
532
679
|
table: tasks,
|
|
533
680
|
// 按操作指定不同的 procedure
|
|
534
681
|
procedure: {
|
|
535
|
-
list: publicProcedure,
|
|
682
|
+
list: publicProcedure, // 公开读
|
|
536
683
|
get: publicProcedure,
|
|
537
684
|
create: protectedProcedure, // 需要登录
|
|
538
685
|
update: protectedProcedure,
|
|
539
|
-
delete: adminProcedure,
|
|
686
|
+
delete: adminProcedure, // 需要管理员
|
|
540
687
|
default: protectedProcedure,
|
|
541
688
|
},
|
|
542
689
|
});
|
|
@@ -549,8 +696,8 @@ export const tasksRouter = createCrudRouter({
|
|
|
549
696
|
table: tasks,
|
|
550
697
|
guard: (ctx, operation) => {
|
|
551
698
|
// 删除操作需要管理员权限
|
|
552
|
-
if (operation ===
|
|
553
|
-
return ctx.user.role ===
|
|
699
|
+
if (operation === 'delete') {
|
|
700
|
+
return ctx.user.role === 'admin';
|
|
554
701
|
}
|
|
555
702
|
// 其他操作需要登录
|
|
556
703
|
return !!ctx.user;
|
|
@@ -561,7 +708,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
561
708
|
### 使用 scope(行级过滤 RLS)
|
|
562
709
|
|
|
563
710
|
```typescript
|
|
564
|
-
import { eq } from
|
|
711
|
+
import { eq } from 'drizzle-orm';
|
|
565
712
|
|
|
566
713
|
export const tasksRouter = createCrudRouter({
|
|
567
714
|
table: tasks,
|
|
@@ -592,7 +739,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
592
739
|
// 写入时强制覆盖字段(防止伪造)
|
|
593
740
|
inject: (ctx, operation) => ({
|
|
594
741
|
tenantId: ctx.user.tenantId,
|
|
595
|
-
...(operation ===
|
|
742
|
+
...(operation === 'create' ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
|
|
596
743
|
}),
|
|
597
744
|
});
|
|
598
745
|
```
|
|
@@ -606,9 +753,9 @@ export const tasksRouter = createCrudRouter({
|
|
|
606
753
|
使用 `.procedures` 属性 spread 出 CRUD 路由,然后添加自定义路由:
|
|
607
754
|
|
|
608
755
|
```typescript
|
|
609
|
-
import { createCrudRouter, router } from
|
|
610
|
-
import { protectedProcedure } from
|
|
611
|
-
import { z } from
|
|
756
|
+
import { createCrudRouter, router } from '@wordrhyme/auto-crud-server';
|
|
757
|
+
import { protectedProcedure } from '../trpc';
|
|
758
|
+
import { z } from 'zod';
|
|
612
759
|
|
|
613
760
|
// 创建基础 CRUD
|
|
614
761
|
const tasksCrud = createCrudRouter({
|
|
@@ -623,20 +770,14 @@ export const tasksRouter = router({
|
|
|
623
770
|
archive: protectedProcedure
|
|
624
771
|
.input(z.object({ id: z.string() }))
|
|
625
772
|
.mutation(async ({ input, ctx }) => {
|
|
626
|
-
return ctx.db
|
|
627
|
-
.update(tasks)
|
|
628
|
-
.set({ archived: true })
|
|
629
|
-
.where(eq(tasks.id, input.id));
|
|
773
|
+
return ctx.db.update(tasks).set({ archived: true }).where(eq(tasks.id, input.id));
|
|
630
774
|
}),
|
|
631
775
|
|
|
632
776
|
// 自定义路由:取消归档
|
|
633
777
|
unarchive: protectedProcedure
|
|
634
778
|
.input(z.object({ id: z.string() }))
|
|
635
779
|
.mutation(async ({ input, ctx }) => {
|
|
636
|
-
return ctx.db
|
|
637
|
-
.update(tasks)
|
|
638
|
-
.set({ archived: false })
|
|
639
|
-
.where(eq(tasks.id, input.id));
|
|
780
|
+
return ctx.db.update(tasks).set({ archived: false }).where(eq(tasks.id, input.id));
|
|
640
781
|
}),
|
|
641
782
|
});
|
|
642
783
|
```
|
|
@@ -659,27 +800,21 @@ export const appRouter = router({
|
|
|
659
800
|
### 自定义过滤逻辑
|
|
660
801
|
|
|
661
802
|
```typescript
|
|
662
|
-
//
|
|
663
|
-
const tasksCrud = createCrudRouter({
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.input(listInputSchema)
|
|
671
|
-
.query(async ({ input, ctx }) => {
|
|
672
|
-
// 自定义过滤逻辑
|
|
673
|
-
const customFilters = input.filters?.map(filter => {
|
|
674
|
-
if (filter.id === "customField") {
|
|
675
|
-
return { ...filter, operator: "custom" };
|
|
803
|
+
// 通过 middleware 调整输入,继续复用内置分页/排序/过滤/count/scope/guard
|
|
804
|
+
const tasksCrud = createCrudRouter({
|
|
805
|
+
table: tasks,
|
|
806
|
+
middleware: {
|
|
807
|
+
list: async ({ input, next }) => {
|
|
808
|
+
const filters = input.filters?.map((filter) => {
|
|
809
|
+
if (filter.id === 'customField') {
|
|
810
|
+
return { ...filter, operator: 'custom' };
|
|
676
811
|
}
|
|
677
812
|
return filter;
|
|
678
813
|
});
|
|
679
814
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
815
|
+
return next({ ...input, filters });
|
|
816
|
+
},
|
|
817
|
+
},
|
|
683
818
|
});
|
|
684
819
|
```
|
|
685
820
|
|
|
@@ -692,7 +827,7 @@ export const tasksRouter = router({
|
|
|
692
827
|
### 完整控制模式
|
|
693
828
|
|
|
694
829
|
```typescript
|
|
695
|
-
import { createCrudRouter } from
|
|
830
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
696
831
|
|
|
697
832
|
const tasksRouter = createCrudRouter({
|
|
698
833
|
table: tasks,
|
|
@@ -709,7 +844,7 @@ const tasksRouter = createCrudRouter({
|
|
|
709
844
|
|
|
710
845
|
// 3. 执行副作用
|
|
711
846
|
await sendNotification(result);
|
|
712
|
-
await logAudit(ctx.user,
|
|
847
|
+
await logAudit(ctx.user, 'create', result);
|
|
713
848
|
|
|
714
849
|
// 4. 返回结果(可修改)
|
|
715
850
|
return result;
|
|
@@ -719,15 +854,15 @@ const tasksRouter = createCrudRouter({
|
|
|
719
854
|
update: async ({ ctx, id, data, existing, next }) => {
|
|
720
855
|
// 检查权限
|
|
721
856
|
if (existing.ownerId !== ctx.user.id) {
|
|
722
|
-
throw new Error(
|
|
857
|
+
throw new Error('Forbidden');
|
|
723
858
|
}
|
|
724
859
|
return next(data);
|
|
725
860
|
},
|
|
726
861
|
|
|
727
862
|
// 删除:条件拦截
|
|
728
863
|
delete: async ({ ctx, id, existing, next }) => {
|
|
729
|
-
if (existing.status ===
|
|
730
|
-
throw new Error(
|
|
864
|
+
if (existing.status === 'locked') {
|
|
865
|
+
throw new Error('Cannot delete locked resource');
|
|
731
866
|
}
|
|
732
867
|
return next();
|
|
733
868
|
},
|
|
@@ -746,7 +881,7 @@ import {
|
|
|
746
881
|
beforeMiddleware,
|
|
747
882
|
afterCreate,
|
|
748
883
|
beforeCreate,
|
|
749
|
-
} from
|
|
884
|
+
} from '@wordrhyme/auto-crud-server';
|
|
750
885
|
|
|
751
886
|
const tasksRouter = createCrudRouter({
|
|
752
887
|
table: tasks,
|
|
@@ -755,7 +890,7 @@ const tasksRouter = createCrudRouter({
|
|
|
755
890
|
// 简单副作用:只在操作后执行
|
|
756
891
|
create: afterMiddleware(async (ctx, result) => {
|
|
757
892
|
await sendEmail(result);
|
|
758
|
-
await logAudit(ctx.user,
|
|
893
|
+
await logAudit(ctx.user, 'create', result);
|
|
759
894
|
}),
|
|
760
895
|
|
|
761
896
|
// 修改输入:只在操作前执行
|
|
@@ -773,15 +908,15 @@ const tasksRouter = createCrudRouter({
|
|
|
773
908
|
|
|
774
909
|
### 可用的工具函数
|
|
775
910
|
|
|
776
|
-
| 函数
|
|
777
|
-
|
|
778
|
-
| `afterMiddleware(fn)`
|
|
779
|
-
| `afterMiddlewareTransform(fn)` | 操作后修改返回值
|
|
780
|
-
| `beforeMiddleware(fn)`
|
|
781
|
-
| `composeMiddleware(...fns)`
|
|
782
|
-
| `afterList`, `beforeList`
|
|
783
|
-
| `afterCreate`, `beforeCreate`
|
|
784
|
-
| `afterUpdate`, `afterDelete`
|
|
911
|
+
| 函数 | 用途 | 示例 |
|
|
912
|
+
| ------------------------------ | ------------------ | -------------------- |
|
|
913
|
+
| `afterMiddleware(fn)` | 操作后执行副作用 | 日志、通知、审计 |
|
|
914
|
+
| `afterMiddlewareTransform(fn)` | 操作后修改返回值 | 添加计算字段 |
|
|
915
|
+
| `beforeMiddleware(fn)` | 操作前修改输入 | 注入用户ID、生成slug |
|
|
916
|
+
| `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
|
|
917
|
+
| `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
|
|
918
|
+
| `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
|
|
919
|
+
| `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
|
|
785
920
|
|
|
786
921
|
---
|
|
787
922
|
|
|
@@ -790,9 +925,9 @@ const tasksRouter = createCrudRouter({
|
|
|
790
925
|
### 与 Drizzle ORM 集成
|
|
791
926
|
|
|
792
927
|
```typescript
|
|
793
|
-
import { drizzle } from
|
|
794
|
-
import postgres from
|
|
795
|
-
import * as schema from
|
|
928
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
929
|
+
import postgres from 'postgres';
|
|
930
|
+
import * as schema from './schema';
|
|
796
931
|
|
|
797
932
|
const client = postgres(process.env.DATABASE_URL!);
|
|
798
933
|
export const db = drizzle(client, { schema });
|
|
@@ -805,12 +940,12 @@ export const createContext = () => ({ db });
|
|
|
805
940
|
|
|
806
941
|
```typescript
|
|
807
942
|
// app/api/trpc/[trpc]/route.ts
|
|
808
|
-
import { fetchRequestHandler } from
|
|
809
|
-
import { appRouter } from
|
|
943
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
944
|
+
import { appRouter } from '@/server/routers';
|
|
810
945
|
|
|
811
946
|
const handler = (req: Request) =>
|
|
812
947
|
fetchRequestHandler({
|
|
813
|
-
endpoint:
|
|
948
|
+
endpoint: '/api/trpc',
|
|
814
949
|
req,
|
|
815
950
|
router: appRouter,
|
|
816
951
|
createContext: () => ({ db }),
|
|
@@ -822,18 +957,18 @@ export { handler as GET, handler as POST };
|
|
|
822
957
|
### 与 Express 集成
|
|
823
958
|
|
|
824
959
|
```typescript
|
|
825
|
-
import express from
|
|
826
|
-
import { createExpressMiddleware } from
|
|
827
|
-
import { appRouter } from
|
|
960
|
+
import express from 'express';
|
|
961
|
+
import { createExpressMiddleware } from '@trpc/server/adapters/express';
|
|
962
|
+
import { appRouter } from './server/routers';
|
|
828
963
|
|
|
829
964
|
const app = express();
|
|
830
965
|
|
|
831
966
|
app.use(
|
|
832
|
-
|
|
967
|
+
'/trpc',
|
|
833
968
|
createExpressMiddleware({
|
|
834
969
|
router: appRouter,
|
|
835
970
|
createContext: () => ({ db }),
|
|
836
|
-
})
|
|
971
|
+
}),
|
|
837
972
|
);
|
|
838
973
|
|
|
839
974
|
app.listen(3000);
|
|
@@ -848,7 +983,7 @@ app.listen(3000);
|
|
|
848
983
|
```typescript
|
|
849
984
|
export const usersRouter = createCrudRouter({
|
|
850
985
|
table: users,
|
|
851
|
-
idField:
|
|
986
|
+
idField: 'userId', // 使用自定义 ID 字段
|
|
852
987
|
});
|
|
853
988
|
```
|
|
854
989
|
|
|
@@ -858,7 +993,7 @@ export const usersRouter = createCrudRouter({
|
|
|
858
993
|
export const ordersRouter = createCrudRouter({
|
|
859
994
|
table: orders,
|
|
860
995
|
// 自动派生 schema 时排除这些字段
|
|
861
|
-
omitFields: [
|
|
996
|
+
omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
|
|
862
997
|
});
|
|
863
998
|
```
|
|
864
999
|
|
|
@@ -871,10 +1006,10 @@ export const tasksRouter = createCrudRouter({
|
|
|
871
1006
|
softDelete: true,
|
|
872
1007
|
|
|
873
1008
|
// 方式 2:指定列名
|
|
874
|
-
softDelete:
|
|
1009
|
+
softDelete: 'deletedAt',
|
|
875
1010
|
|
|
876
1011
|
// 方式 3:完整配置(用于布尔字段)
|
|
877
|
-
softDelete: { column:
|
|
1012
|
+
softDelete: { column: 'isDeleted', value: () => true },
|
|
878
1013
|
});
|
|
879
1014
|
```
|
|
880
1015
|
|
|
@@ -883,7 +1018,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
883
1018
|
```typescript
|
|
884
1019
|
export const tasksRouter = createCrudRouter({
|
|
885
1020
|
table: tasks,
|
|
886
|
-
maxBatchSize: 50,
|
|
1021
|
+
maxBatchSize: 50, // 默认 100
|
|
887
1022
|
});
|
|
888
1023
|
```
|
|
889
1024
|
|
|
@@ -892,8 +1027,57 @@ export const tasksRouter = createCrudRouter({
|
|
|
892
1027
|
```typescript
|
|
893
1028
|
export const tasksRouter = createCrudRouter({
|
|
894
1029
|
table: tasks,
|
|
895
|
-
filterableColumns: [
|
|
896
|
-
sortableColumns: [
|
|
1030
|
+
filterableColumns: ['title', 'status', 'priority'], // 只允许这些列过滤
|
|
1031
|
+
sortableColumns: ['title', 'createdAt', 'priority'], // 只允许这些列排序
|
|
1032
|
+
});
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
普通字段不需要特殊配置,前端仍然传字段 id:
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
filters: [{ id: 'title', value: 'bug', operator: 'iLike', variant: 'text', filterId: 'title' }],
|
|
1039
|
+
sort: [{ id: 'createdAt', desc: true }],
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
jsonb/i18n 字段可以在对应字段配置里声明读取哪个 JSON field。服务端会显式生成
|
|
1043
|
+
text expression,不会自动把所有 jsonb 字段都 cast 成 text:
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
export const productsRouter = createCrudRouter({
|
|
1047
|
+
table: shopProducts,
|
|
1048
|
+
idField: 'spuId',
|
|
1049
|
+
filterableColumns: [
|
|
1050
|
+
{ id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
1051
|
+
'status',
|
|
1052
|
+
'categoryId',
|
|
1053
|
+
],
|
|
1054
|
+
sortableColumns: [
|
|
1055
|
+
{ id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
1056
|
+
'createdAt',
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
上面的 `name` 过滤会生成类似 SQL:
|
|
1062
|
+
|
|
1063
|
+
```sql
|
|
1064
|
+
case jsonb_typeof(name)
|
|
1065
|
+
when 'object' then coalesce(name ->> 'zh-CN', name ->> 'en-US', name ->> 'en', name ->> 'zh', '')
|
|
1066
|
+
when 'string' then name #>> '{}'
|
|
1067
|
+
else ''
|
|
1068
|
+
end ilike '%关键词%'
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
复杂业务仍可用 `expression` 显式接管查询目标:
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
import { sql } from 'drizzle-orm';
|
|
1075
|
+
|
|
1076
|
+
createCrudRouter({
|
|
1077
|
+
table: products,
|
|
1078
|
+
filterableColumns: [
|
|
1079
|
+
{ id: 'displayName', expression: ({ table }) => sql<string>`lower(${table.name})` },
|
|
1080
|
+
],
|
|
897
1081
|
});
|
|
898
1082
|
```
|
|
899
1083
|
|
|
@@ -903,13 +1087,24 @@ export const tasksRouter = createCrudRouter({
|
|
|
903
1087
|
|
|
904
1088
|
```typescript
|
|
905
1089
|
// 主要导出
|
|
906
|
-
export {
|
|
1090
|
+
export {
|
|
1091
|
+
baseExportInputSchema,
|
|
1092
|
+
baseGetInputSchema,
|
|
1093
|
+
baseListInputSchema,
|
|
1094
|
+
createCrudRouter,
|
|
1095
|
+
} from './routers/_factory';
|
|
907
1096
|
export type {
|
|
1097
|
+
CrudColumnConfig,
|
|
1098
|
+
CrudColumnExpression,
|
|
1099
|
+
CrudColumnRef,
|
|
908
1100
|
CrudRouterConfig,
|
|
909
1101
|
CrudMiddleware,
|
|
1102
|
+
GetInput,
|
|
910
1103
|
ListInput,
|
|
911
1104
|
ListResult,
|
|
912
|
-
|
|
1105
|
+
ExportInput,
|
|
1106
|
+
ExportResult,
|
|
1107
|
+
} from './types/config';
|
|
913
1108
|
|
|
914
1109
|
// Middleware 工具函数
|
|
915
1110
|
export {
|
|
@@ -923,14 +1118,14 @@ export {
|
|
|
923
1118
|
beforeCreate,
|
|
924
1119
|
afterUpdate,
|
|
925
1120
|
afterDelete,
|
|
926
|
-
} from
|
|
1121
|
+
} from './lib/middleware-helpers';
|
|
927
1122
|
|
|
928
1123
|
// tRPC 工具
|
|
929
|
-
export { router, publicProcedure } from
|
|
1124
|
+
export { router, publicProcedure } from './trpc';
|
|
930
1125
|
|
|
931
1126
|
// 示例路由(可选)
|
|
932
|
-
export { appRouter } from
|
|
933
|
-
export type { AppRouter } from
|
|
1127
|
+
export { appRouter } from './routers';
|
|
1128
|
+
export type { AppRouter } from './routers';
|
|
934
1129
|
```
|
|
935
1130
|
|
|
936
1131
|
---
|
|
@@ -942,6 +1137,7 @@ export type { AppRouter } from "./routers";
|
|
|
942
1137
|
**错误**: `Cannot find module '@wordrhyme/auto-crud-server'`
|
|
943
1138
|
|
|
944
1139
|
**解决方案**:
|
|
1140
|
+
|
|
945
1141
|
```bash
|
|
946
1142
|
pnpm install @wordrhyme/auto-crud-server
|
|
947
1143
|
```
|
|
@@ -951,6 +1147,7 @@ pnpm install @wordrhyme/auto-crud-server
|
|
|
951
1147
|
**错误**: `Type 'X' is not assignable to type 'Y'`
|
|
952
1148
|
|
|
953
1149
|
**解决方案**: 确保 Zod 版本一致
|
|
1150
|
+
|
|
954
1151
|
```bash
|
|
955
1152
|
pnpm list zod
|
|
956
1153
|
# 确保所有包使用相同的 Zod 版本
|
|
@@ -961,9 +1158,10 @@ pnpm list zod
|
|
|
961
1158
|
**错误**: `connect ECONNREFUSED`
|
|
962
1159
|
|
|
963
1160
|
**解决方案**: 检查数据库连接字符串
|
|
1161
|
+
|
|
964
1162
|
```typescript
|
|
965
1163
|
// .env
|
|
966
|
-
DATABASE_URL=
|
|
1164
|
+
DATABASE_URL = 'postgresql://user:password@localhost:5432/dbname';
|
|
967
1165
|
```
|
|
968
1166
|
|
|
969
1167
|
---
|