@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 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 "drizzle-orm/pg-core";
55
+ import { pgTable, varchar, real, boolean, timestamp } from 'drizzle-orm/pg-core';
56
56
 
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
- }).notNull().default("todo"),
63
- priority: varchar("priority", {
64
- enum: ["low", "medium", "high"],
65
- }).notNull().default("low"),
66
- estimatedHours: real("estimated_hours").default(0),
67
- createdAt: timestamp("created_at").defaultNow().notNull(),
68
- updatedAt: timestamp("updated_at").defaultNow(),
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 "@wordrhyme/auto-crud-server";
77
- import { tasks } from "@/db/schema";
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 "@wordrhyme/auto-crud-server";
90
- import { tasks } from "@/db/schema";
91
- import { createSelectSchema } from "drizzle-zod";
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, // 主 Schema(用于 create/upsert)
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 "../trpc";
112
- import { tasksRouter } from "./tasks";
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 "@trpc/server/adapters/fetch";
126
- import { appRouter } from "@/server/routers";
127
- import { db } from "@/db";
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: "/api/trpc",
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: "createdAt", desc: true }],
187
+ sort: [{ id: 'createdAt', desc: true }],
181
188
  filters: [
182
- { id: "status", value: "done", operator: "eq", variant: "select" },
183
- { id: "priority", value: "high", operator: "eq", variant: "select" },
189
+ { id: 'status', value: 'done', operator: 'eq', variant: 'select' },
190
+ { id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
184
191
  ],
185
- joinOperator: "and",
192
+ joinOperator: 'and',
186
193
  });
187
194
  ```
188
195
 
189
196
  ### 2. `get` - 单条查询
190
197
 
191
198
  **输入**:
199
+
192
200
  ```typescript
193
- { id: string }
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: "123" });
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, "id" | "createdAt" | "updatedAt">
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: "New Task",
222
- status: "todo",
223
- priority: "high",
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, "id" | "createdAt" | "updatedAt">>;
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: "123",
246
- data: { status: "done" },
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
- { id: string }
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: "123" });
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: ["1", "2", "3"] });
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
- { updated: number }
324
+ {
325
+ updated: number;
326
+ }
297
327
  ```
298
328
 
299
329
  **示例**:
330
+
300
331
  ```typescript
301
332
  await trpc.tasks.updateMany({
302
- ids: ["1", "2", "3"],
303
- data: { status: "done" },
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, "createdAt" | "updatedAt"> // 需要包含 id
343
+ Omit<Task, 'createdAt' | 'updatedAt'>; // 需要包含 id
312
344
  ```
313
345
 
314
346
  **输出**:
347
+
315
348
  ```typescript
316
349
  {
317
- data: Task; // 创建或更新后的记录
318
- isNew: boolean; // true = 新建, false = 更新
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: "123",
327
- title: "My Task",
328
- status: "todo",
360
+ id: '123',
361
+ title: 'My Task',
362
+ status: 'todo',
329
363
  });
330
364
 
331
365
  if (isNew) {
332
- console.log("Created new task");
366
+ console.log('Created new task');
333
367
  } else {
334
- console.log("Updated existing task");
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` | 等于 | `status = "done"` |
352
- | `ne` | 不等于 | `status != "canceled"` |
353
- | `gt` | 大于 | `estimatedHours > 5` |
354
- | `gte` | 大于等于 | `estimatedHours >= 5` |
355
- | `lt` | 小于 | `estimatedHours < 10` |
356
- | `lte` | 小于等于 | `estimatedHours <= 10` |
357
- | `like` | 包含 | `title LIKE "%bug%"` |
358
- | `notLike` | 不包含 | `title NOT LIKE "%test%"` |
359
- | `in` | 在列表中 | `status IN ["todo", "in-progress"]` |
360
- | `notIn` | 不在列表中 | `status NOT IN ["canceled"]` |
361
- | `between` | 范围 | `createdAt BETWEEN "2024-01-01" AND "2024-12-31"` |
362
- | `isNull` | 为空 | `description IS NULL` |
363
- | `isNotNull` | 不为空 | `description IS NOT NULL` |
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: "status", value: "done", operator: "eq", variant: "select" },
387
- { id: "priority", value: "high", operator: "eq", variant: "select" },
419
+ { id: 'status', value: 'done', operator: 'eq', variant: 'select' },
420
+ { id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
388
421
  ],
389
- joinOperator: "and", // status = "done" AND priority = "high"
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: "status", value: "todo", operator: "eq", variant: "select" },
401
- { id: "status", value: "in-progress", operator: "eq", variant: "select" },
433
+ { id: 'status', value: 'todo', operator: 'eq', variant: 'select' },
434
+ { id: 'status', value: 'in-progress', operator: 'eq', variant: 'select' },
402
435
  ],
403
- joinOperator: "or", // status = "todo" OR status = "in-progress"
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: "createdAt",
416
- value: ["2024-01-01", "2024-12-31"],
417
- operator: "between",
418
- variant: "dateRange",
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; // Drizzle 表定义
480
+ table: TTable; // Drizzle 表定义
450
481
 
451
482
  // ========== Schema 配置(可选) ==========
452
- schema?: z.ZodType<TInsert>; // 主 Schema(用于 create/upsert)
453
- updateSchema?: z.ZodType<TUpdate>;// 更新 Schema(覆盖自动派生)
454
- selectSchema?: z.ZodType<TSelect>;// 查询返回 Schema
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; // ID 字段名,默认 "id"
458
- omitFields?: string[]; // 自动派生时排除的字段
459
- // 默认 ["id", "createdAt", "updatedAt"]
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` | 从 `table` 自动派生,排除 `omitFields` |
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: ["id", "createdAt", "updatedAt", "internalCode"],
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 "@wordrhyme/auto-crud-server";
529
- import { protectedProcedure, adminProcedure, publicProcedure } from "../trpc";
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 === "delete") {
553
- return ctx.user.role === "admin";
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 "drizzle-orm";
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 === "create" ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
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 "@wordrhyme/auto-crud-server";
610
- import { protectedProcedure } from "../trpc";
611
- import { z } from "zod";
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
- // 覆盖 list 路由,添加自定义过滤逻辑
663
- const tasksCrud = createCrudRouter({ table: tasks });
664
-
665
- export const tasksRouter = router({
666
- ...tasksCrud.procedures,
667
-
668
- // 覆盖 list,添加自定义逻辑
669
- list: protectedProcedure
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
- // 调用原始 list(需要手动实现或使用 db 查询)
681
- return ctx.db.select().from(tasks).where(...);
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 "@wordrhyme/auto-crud-server";
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, "create", result);
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("Forbidden");
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 === "locked") {
730
- throw new Error("Cannot delete locked resource");
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 "@wordrhyme/auto-crud-server";
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, "create", result);
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)` | 操作前修改输入 | 注入用户ID、生成slug |
781
- | `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
782
- | `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
783
- | `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
784
- | `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
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 "drizzle-orm/postgres-js";
794
- import postgres from "postgres";
795
- import * as schema from "./schema";
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 "@trpc/server/adapters/fetch";
809
- import { appRouter } from "@/server/routers";
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: "/api/trpc",
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 "express";
826
- import { createExpressMiddleware } from "@trpc/server/adapters/express";
827
- import { appRouter } from "./server/routers";
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
- "/trpc",
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: "userId", // 使用自定义 ID 字段
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: ["id", "createdAt", "updatedAt", "internalCode"],
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: "deletedAt",
1009
+ softDelete: 'deletedAt',
875
1010
 
876
1011
  // 方式 3:完整配置(用于布尔字段)
877
- softDelete: { column: "isDeleted", value: () => true },
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, // 默认 100
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: ["title", "status", "priority"], // 只允许这些列过滤
896
- sortableColumns: ["title", "createdAt", "priority"], // 只允许这些列排序
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 { createCrudRouter } from "./routers/_factory";
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
- } from "./types/config";
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 "./lib/middleware-helpers";
1121
+ } from './lib/middleware-helpers';
927
1122
 
928
1123
  // tRPC 工具
929
- export { router, publicProcedure } from "./trpc";
1124
+ export { router, publicProcedure } from './trpc';
930
1125
 
931
1126
  // 示例路由(可选)
932
- export { appRouter } from "./routers";
933
- export type { AppRouter } from "./routers";
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="postgresql://user:password@localhost:5432/dbname"
1164
+ DATABASE_URL = 'postgresql://user:password@localhost:5432/dbname';
967
1165
  ```
968
1166
 
969
1167
  ---