@wordrhyme/auto-crud-server 0.8.0 → 0.10.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/README.md +161 -95
- package/dist/index.cjs +204 -11
- package/dist/index.d.cts +178 -19
- package/dist/index.d.ts +177 -18
- package/dist/index.js +203 -11
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -75,27 +75,32 @@ export const tasks = pgTable("tasks", {
|
|
|
75
75
|
// src/server/routers/tasks.ts
|
|
76
76
|
import { createCrudRouter } from "@wordrhyme/auto-crud-server";
|
|
77
77
|
import { tasks } from "@/db/schema";
|
|
78
|
+
|
|
79
|
+
// 🚀 零配置!一行代码生成完整 CRUD 路由
|
|
80
|
+
export const tasksRouter = createCrudRouter({
|
|
81
|
+
table: tasks,
|
|
82
|
+
// Schema 自动从 table 派生,排除 id, createdAt, updatedAt
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**或者,显式传入 Schema:**
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { createCrudRouter } from "@wordrhyme/auto-crud-server";
|
|
90
|
+
import { tasks } from "@/db/schema";
|
|
78
91
|
import { createSelectSchema } from "drizzle-zod";
|
|
79
92
|
|
|
80
93
|
// 从 Drizzle Schema 自动生成 Zod Schema
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
// 创建输入 Schema
|
|
84
|
-
const insertTaskSchema = selectTaskSchema.omit({
|
|
94
|
+
const taskSchema = createSelectSchema(tasks).omit({
|
|
85
95
|
id: true,
|
|
86
96
|
createdAt: true,
|
|
87
97
|
updatedAt: true,
|
|
88
98
|
});
|
|
89
99
|
|
|
90
|
-
const updateTaskSchema = insertTaskSchema.partial();
|
|
91
|
-
|
|
92
|
-
// 🚀 一行代码生成完整 CRUD 路由
|
|
93
100
|
export const tasksRouter = createCrudRouter({
|
|
94
101
|
table: tasks,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
updateSchema: updateTaskSchema,
|
|
98
|
-
idField: "id", // 可选,默认为 "id"
|
|
102
|
+
schema: taskSchema, // 主 Schema(用于 create/upsert)
|
|
103
|
+
// updateSchema 自动派生为 schema.partial()
|
|
99
104
|
});
|
|
100
105
|
```
|
|
101
106
|
|
|
@@ -440,14 +445,57 @@ await trpc.tasks.list({
|
|
|
440
445
|
|
|
441
446
|
```typescript
|
|
442
447
|
interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
|
|
448
|
+
// ========== 必填 ==========
|
|
443
449
|
table: TTable; // Drizzle 表定义
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
|
|
451
|
+
// ========== Schema 配置(可选) ==========
|
|
452
|
+
schema?: z.ZodType<TInsert>; // 主 Schema(用于 create/upsert)
|
|
453
|
+
updateSchema?: z.ZodType<TUpdate>;// 更新 Schema(覆盖自动派生)
|
|
454
|
+
selectSchema?: z.ZodType<TSelect>;// 查询返回 Schema
|
|
455
|
+
|
|
456
|
+
// ========== 其他配置 ==========
|
|
447
457
|
idField?: string; // ID 字段名,默认 "id"
|
|
458
|
+
omitFields?: string[]; // 自动派生时排除的字段
|
|
459
|
+
// 默认 ["id", "createdAt", "updatedAt"]
|
|
448
460
|
}
|
|
449
461
|
```
|
|
450
462
|
|
|
463
|
+
#### Schema 派生规则
|
|
464
|
+
|
|
465
|
+
| 配置 | 派生行为 |
|
|
466
|
+
|-----|---------|
|
|
467
|
+
| 无 `schema` | 从 `table` 自动派生,排除 `omitFields` |
|
|
468
|
+
| 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
|
|
469
|
+
| 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
|
|
470
|
+
|
|
471
|
+
#### 使用示例
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// 1. 零配置 - 全部自动派生
|
|
475
|
+
const tasksRouter = createCrudRouter({
|
|
476
|
+
table: tasks,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// 2. 传入 schema,updateSchema 自动派生
|
|
480
|
+
const postsRouter = createCrudRouter({
|
|
481
|
+
table: posts,
|
|
482
|
+
schema: postSchema,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// 3. 传入 schema + 自定义 updateSchema
|
|
486
|
+
const usersRouter = createCrudRouter({
|
|
487
|
+
table: users,
|
|
488
|
+
schema: userSchema,
|
|
489
|
+
updateSchema: customUpdateSchema,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// 4. 自定义排除字段
|
|
493
|
+
const ordersRouter = createCrudRouter({
|
|
494
|
+
table: orders,
|
|
495
|
+
omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
451
499
|
#### 返回值
|
|
452
500
|
|
|
453
501
|
```typescript
|
|
@@ -459,95 +507,94 @@ interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
|
|
|
459
507
|
// 可 spread 的 procedures 对象(用于扩展自定义路由)
|
|
460
508
|
procedures: {
|
|
461
509
|
list: Procedure<ListInput, ListOutput>,
|
|
462
|
-
|
|
510
|
+
get: Procedure<string, TSelect>,
|
|
463
511
|
create: Procedure<TInsert, TSelect>,
|
|
464
512
|
update: Procedure<{ id: string, data: TUpdate }, TSelect>,
|
|
465
513
|
delete: Procedure<string, TSelect>,
|
|
466
514
|
deleteMany: Procedure<string[], { deleted: number }>,
|
|
467
515
|
updateMany: Procedure<{ ids: string[], data: TUpdate }, { updated: number }>,
|
|
516
|
+
upsert: Procedure<TInsert, { data: TSelect, isNew: boolean }>,
|
|
468
517
|
}
|
|
469
518
|
}
|
|
470
519
|
```
|
|
471
520
|
|
|
472
|
-
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## 🔐 权限控制
|
|
524
|
+
|
|
525
|
+
### 使用 procedure 配置
|
|
473
526
|
|
|
474
527
|
```typescript
|
|
475
528
|
import { createCrudRouter } from "@wordrhyme/auto-crud-server";
|
|
476
|
-
import {
|
|
477
|
-
import { createSelectSchema } from "drizzle-zod";
|
|
478
|
-
import { z } from "zod";
|
|
529
|
+
import { protectedProcedure, adminProcedure, publicProcedure } from "../trpc";
|
|
479
530
|
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
531
|
+
export const tasksRouter = createCrudRouter({
|
|
532
|
+
table: tasks,
|
|
533
|
+
// 按操作指定不同的 procedure
|
|
534
|
+
procedure: {
|
|
535
|
+
list: publicProcedure, // 公开读
|
|
536
|
+
get: publicProcedure,
|
|
537
|
+
create: protectedProcedure, // 需要登录
|
|
538
|
+
update: protectedProcedure,
|
|
539
|
+
delete: adminProcedure, // 需要管理员
|
|
540
|
+
default: protectedProcedure,
|
|
541
|
+
},
|
|
486
542
|
});
|
|
543
|
+
```
|
|
487
544
|
|
|
488
|
-
|
|
545
|
+
### 使用 guard(操作级守卫)
|
|
489
546
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
547
|
+
```typescript
|
|
548
|
+
export const tasksRouter = createCrudRouter({
|
|
549
|
+
table: tasks,
|
|
550
|
+
guard: (ctx, operation) => {
|
|
551
|
+
// 删除操作需要管理员权限
|
|
552
|
+
if (operation === "delete") {
|
|
553
|
+
return ctx.user.role === "admin";
|
|
554
|
+
}
|
|
555
|
+
// 其他操作需要登录
|
|
556
|
+
return !!ctx.user;
|
|
557
|
+
},
|
|
496
558
|
});
|
|
497
559
|
```
|
|
498
560
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
## 🔐 权限控制
|
|
502
|
-
|
|
503
|
-
### 使用 tRPC Middleware
|
|
561
|
+
### 使用 scope(行级过滤 RLS)
|
|
504
562
|
|
|
505
563
|
```typescript
|
|
506
|
-
import {
|
|
507
|
-
|
|
508
|
-
const t = initTRPC.context<Context>().create();
|
|
564
|
+
import { eq } from "drizzle-orm";
|
|
509
565
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
566
|
+
export const tasksRouter = createCrudRouter({
|
|
567
|
+
table: tasks,
|
|
568
|
+
// 自动注入到所有查询的 WHERE 子句
|
|
569
|
+
scope: (ctx, table, operation) => {
|
|
570
|
+
return eq(table.tenantId, ctx.user.tenantId);
|
|
571
|
+
},
|
|
516
572
|
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 使用 authorize(资源级检查 ABAC)
|
|
517
576
|
|
|
518
|
-
|
|
577
|
+
```typescript
|
|
519
578
|
export const tasksRouter = createCrudRouter({
|
|
520
579
|
table: tasks,
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// 需要在路由层面添加权限控制
|
|
580
|
+
// 在 get/update/delete 时,预取资源后调用
|
|
581
|
+
authorize: (ctx, resource, operation) => {
|
|
582
|
+
return resource.ownerId === ctx.user.id;
|
|
583
|
+
},
|
|
526
584
|
});
|
|
527
|
-
|
|
528
|
-
// 包装路由添加权限
|
|
529
|
-
export const protectedTasksRouter = {
|
|
530
|
-
list: protectedProcedure.query(async ({ input, ctx }) => {
|
|
531
|
-
return tasksRouter.list(input, ctx);
|
|
532
|
-
}),
|
|
533
|
-
// ... 其他路由
|
|
534
|
-
};
|
|
535
585
|
```
|
|
536
586
|
|
|
537
|
-
###
|
|
587
|
+
### 使用 inject(强制注入字段)
|
|
538
588
|
|
|
539
589
|
```typescript
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
.select()
|
|
549
|
-
.from(tasks)
|
|
550
|
-
.where(eq(tasks.userId, ctx.user.id)); // 只查询当前用户的任务
|
|
590
|
+
export const tasksRouter = createCrudRouter({
|
|
591
|
+
table: tasks,
|
|
592
|
+
// 写入时强制覆盖字段(防止伪造)
|
|
593
|
+
inject: (ctx, operation) => ({
|
|
594
|
+
tenantId: ctx.user.tenantId,
|
|
595
|
+
...(operation === "create" ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
551
598
|
```
|
|
552
599
|
|
|
553
600
|
---
|
|
@@ -566,9 +613,6 @@ import { z } from "zod";
|
|
|
566
613
|
// 创建基础 CRUD
|
|
567
614
|
const tasksCrud = createCrudRouter({
|
|
568
615
|
table: tasks,
|
|
569
|
-
selectSchema: selectTaskSchema,
|
|
570
|
-
insertSchema: insertTaskSchema,
|
|
571
|
-
updateSchema: updateTaskSchema,
|
|
572
616
|
});
|
|
573
617
|
|
|
574
618
|
// 使用 .procedures spread 并添加自定义路由
|
|
@@ -604,9 +648,6 @@ export const tasksRouter = router({
|
|
|
604
648
|
```typescript
|
|
605
649
|
const tasksCrud = createCrudRouter({
|
|
606
650
|
table: tasks,
|
|
607
|
-
selectSchema: selectTaskSchema,
|
|
608
|
-
insertSchema: insertTaskSchema,
|
|
609
|
-
updateSchema: updateTaskSchema,
|
|
610
651
|
});
|
|
611
652
|
|
|
612
653
|
// 直接嵌套到 appRouter
|
|
@@ -619,7 +660,7 @@ export const appRouter = router({
|
|
|
619
660
|
|
|
620
661
|
```typescript
|
|
621
662
|
// 覆盖 list 路由,添加自定义过滤逻辑
|
|
622
|
-
const tasksCrud = createCrudRouter({
|
|
663
|
+
const tasksCrud = createCrudRouter({ table: tasks });
|
|
623
664
|
|
|
624
665
|
export const tasksRouter = router({
|
|
625
666
|
...tasksCrud.procedures,
|
|
@@ -655,8 +696,6 @@ import { createCrudRouter } from "@wordrhyme/auto-crud-server";
|
|
|
655
696
|
|
|
656
697
|
const tasksRouter = createCrudRouter({
|
|
657
698
|
table: tasks,
|
|
658
|
-
insertSchema: insertTaskSchema,
|
|
659
|
-
updateSchema: updateTaskSchema,
|
|
660
699
|
|
|
661
700
|
// 中间件:完全控制操作流程
|
|
662
701
|
middleware: {
|
|
@@ -711,8 +750,6 @@ import {
|
|
|
711
750
|
|
|
712
751
|
const tasksRouter = createCrudRouter({
|
|
713
752
|
table: tasks,
|
|
714
|
-
insertSchema: insertTaskSchema,
|
|
715
|
-
updateSchema: updateTaskSchema,
|
|
716
753
|
|
|
717
754
|
middleware: {
|
|
718
755
|
// 简单副作用:只在操作后执行
|
|
@@ -811,24 +848,53 @@ app.listen(3000);
|
|
|
811
848
|
```typescript
|
|
812
849
|
export const usersRouter = createCrudRouter({
|
|
813
850
|
table: users,
|
|
814
|
-
selectSchema: selectUserSchema,
|
|
815
|
-
insertSchema: insertUserSchema,
|
|
816
|
-
updateSchema: updateUserSchema,
|
|
817
851
|
idField: "userId", // 使用自定义 ID 字段
|
|
818
852
|
});
|
|
819
853
|
```
|
|
820
854
|
|
|
821
|
-
###
|
|
855
|
+
### 自定义排除字段
|
|
822
856
|
|
|
823
857
|
```typescript
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
858
|
+
export const ordersRouter = createCrudRouter({
|
|
859
|
+
table: orders,
|
|
860
|
+
// 自动派生 schema 时排除这些字段
|
|
861
|
+
omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### 软删除
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
export const tasksRouter = createCrudRouter({
|
|
869
|
+
table: tasks,
|
|
870
|
+
// 方式 1:使用默认列名 'deletedAt'
|
|
871
|
+
softDelete: true,
|
|
872
|
+
|
|
873
|
+
// 方式 2:指定列名
|
|
874
|
+
softDelete: "deletedAt",
|
|
875
|
+
|
|
876
|
+
// 方式 3:完整配置(用于布尔字段)
|
|
877
|
+
softDelete: { column: "isDeleted", value: () => true },
|
|
878
|
+
});
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### 批量操作限制
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
export const tasksRouter = createCrudRouter({
|
|
885
|
+
table: tasks,
|
|
886
|
+
maxBatchSize: 50, // 默认 100
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### 列白名单
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
export const tasksRouter = createCrudRouter({
|
|
894
|
+
table: tasks,
|
|
895
|
+
filterableColumns: ["title", "status", "priority"], // 只允许这些列过滤
|
|
896
|
+
sortableColumns: ["title", "createdAt", "priority"], // 只允许这些列排序
|
|
897
|
+
});
|
|
832
898
|
```
|
|
833
899
|
|
|
834
900
|
---
|
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,8 @@ let zod = require("zod");
|
|
|
25
25
|
zod = __toESM(zod);
|
|
26
26
|
let drizzle_orm = require("drizzle-orm");
|
|
27
27
|
drizzle_orm = __toESM(drizzle_orm);
|
|
28
|
+
let drizzle_zod = require("drizzle-zod");
|
|
29
|
+
drizzle_zod = __toESM(drizzle_zod);
|
|
28
30
|
let __trpc_server = require("@trpc/server");
|
|
29
31
|
__trpc_server = __toESM(__trpc_server);
|
|
30
32
|
let superjson = require("superjson");
|
|
@@ -399,6 +401,26 @@ const dataTableConfig = {
|
|
|
399
401
|
joinOperators: ["and", "or"]
|
|
400
402
|
};
|
|
401
403
|
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/lib/schema-utils.ts
|
|
406
|
+
/**
|
|
407
|
+
* 检查是否为 ZodObject 类型
|
|
408
|
+
* 兼容 Zod v3 和 v4
|
|
409
|
+
*
|
|
410
|
+
* 实现策略:检查 schema 是否具有 ZodObject 特有的 .shape 属性
|
|
411
|
+
* 这种方式比检查内部 _def 属性更稳定,因为不依赖 Zod 的内部实现细节
|
|
412
|
+
*/
|
|
413
|
+
function isZodObject(schema) {
|
|
414
|
+
return schema !== null && typeof schema === "object" && "shape" in schema && typeof schema.shape === "object" && schema.shape !== null;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 非空对象验证器
|
|
418
|
+
* 用于 updateSchema 的 refine,防止空更新
|
|
419
|
+
*/
|
|
420
|
+
function nonEmpty(value) {
|
|
421
|
+
return Object.keys(value).length > 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
402
424
|
//#endregion
|
|
403
425
|
//#region src/routers/_factory.ts
|
|
404
426
|
function resolveSoftDelete(option) {
|
|
@@ -434,6 +456,9 @@ function isProcedureMap(config) {
|
|
|
434
456
|
"deleteMany",
|
|
435
457
|
"updateMany",
|
|
436
458
|
"upsert",
|
|
459
|
+
"export",
|
|
460
|
+
"import",
|
|
461
|
+
"createMany",
|
|
437
462
|
"default"
|
|
438
463
|
];
|
|
439
464
|
return keys.some((k) => validKeys.includes(k));
|
|
@@ -485,20 +510,63 @@ const listInputSchema = zod.z.object({
|
|
|
485
510
|
filters: zod.z.array(filterItemSchema).optional(),
|
|
486
511
|
joinOperator: zod.z.enum(["and", "or"]).default("and")
|
|
487
512
|
});
|
|
513
|
+
const exportInputSchema = zod.z.object({
|
|
514
|
+
sort: zod.z.array(zod.z.object({
|
|
515
|
+
id: zod.z.string(),
|
|
516
|
+
desc: zod.z.boolean()
|
|
517
|
+
})).optional(),
|
|
518
|
+
filters: zod.z.array(filterItemSchema).optional(),
|
|
519
|
+
joinOperator: zod.z.enum(["and", "or"]).default("and"),
|
|
520
|
+
limit: zod.z.number().min(1).optional()
|
|
521
|
+
});
|
|
522
|
+
const importInputSchema = zod.z.object({
|
|
523
|
+
rows: zod.z.array(zod.z.unknown()).min(1).max(1e3),
|
|
524
|
+
onConflict: zod.z.enum([
|
|
525
|
+
"skip",
|
|
526
|
+
"upsert",
|
|
527
|
+
"error"
|
|
528
|
+
]).default("skip")
|
|
529
|
+
});
|
|
530
|
+
const DEFAULT_OMIT_FIELDS = [
|
|
531
|
+
"id",
|
|
532
|
+
"createdAt",
|
|
533
|
+
"updatedAt"
|
|
534
|
+
];
|
|
535
|
+
/**
|
|
536
|
+
* 解析并派生 Schema
|
|
537
|
+
* - 优先使用显式传入的 Schema
|
|
538
|
+
* - 否则从 Drizzle table 自动派生
|
|
539
|
+
*/
|
|
540
|
+
function resolveSchemas(config) {
|
|
541
|
+
const { table, omitFields = DEFAULT_OMIT_FIELDS } = config;
|
|
542
|
+
let schema;
|
|
543
|
+
if (config.schema) {
|
|
544
|
+
if (!isZodObject(config.schema)) throw new Error("[createCrudRouter] schema must be a ZodObject (not ZodEffects or other types). If you're using .refine() or .transform(), please remove them from schema.");
|
|
545
|
+
schema = config.schema;
|
|
546
|
+
} else {
|
|
547
|
+
const rawSchema = (0, drizzle_zod.createInsertSchema)(table);
|
|
548
|
+
if (!isZodObject(rawSchema)) throw new Error("[createCrudRouter] drizzle-zod returned non-ZodObject schema. This may be due to table refinements. Please provide schema explicitly.");
|
|
549
|
+
const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
|
|
550
|
+
schema = rawSchema.omit(omitConfig);
|
|
551
|
+
}
|
|
552
|
+
let selectSchema;
|
|
553
|
+
if (config.selectSchema) selectSchema = config.selectSchema;
|
|
554
|
+
else if (config.schema) selectSchema = schema;
|
|
555
|
+
else selectSchema = (0, drizzle_zod.createSelectSchema)(table);
|
|
556
|
+
const updateSchema = config.updateSchema ?? schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
|
|
557
|
+
return {
|
|
558
|
+
selectSchema,
|
|
559
|
+
schema,
|
|
560
|
+
updateSchema
|
|
561
|
+
};
|
|
562
|
+
}
|
|
488
563
|
function validateColumn(columnId, allowedColumns) {
|
|
489
564
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
490
565
|
return allowedColumns.includes(columnId);
|
|
491
566
|
}
|
|
492
|
-
/**
|
|
493
|
-
* 创建 CRUD Router
|
|
494
|
-
*
|
|
495
|
-
* @typeParam TContext - 上下文类型(包含 db)
|
|
496
|
-
* @typeParam TSelect - 查询返回类型(从 selectSchema 或 insertSchema 推断)
|
|
497
|
-
* @typeParam TInsert - 创建输入类型(从 insertSchema 推断)
|
|
498
|
-
* @typeParam TUpdate - 更新输入类型(从 updateSchema 推断)
|
|
499
|
-
*/
|
|
500
567
|
function createCrudRouter(config) {
|
|
501
|
-
const { table,
|
|
568
|
+
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
569
|
+
const { schema, updateSchema } = resolveSchemas(config);
|
|
502
570
|
const resolved = resolveConfig(config);
|
|
503
571
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
504
572
|
const m = middleware;
|
|
@@ -596,7 +664,7 @@ function createCrudRouter(config) {
|
|
|
596
664
|
});
|
|
597
665
|
return doGet();
|
|
598
666
|
}),
|
|
599
|
-
create: resolved.procedureFactory("create").input(
|
|
667
|
+
create: resolved.procedureFactory("create").input(schema).mutation(async ({ ctx, input }) => {
|
|
600
668
|
await withGuard(ctx, "create");
|
|
601
669
|
const doCreate = async (inputData) => {
|
|
602
670
|
const injectData = resolved.getInject(ctx, "create");
|
|
@@ -703,7 +771,7 @@ function createCrudRouter(config) {
|
|
|
703
771
|
});
|
|
704
772
|
return doUpdateMany(input.data);
|
|
705
773
|
}),
|
|
706
|
-
upsert: resolved.procedureFactory("upsert").input(
|
|
774
|
+
upsert: resolved.procedureFactory("upsert").input(schema).mutation(async ({ ctx, input }) => {
|
|
707
775
|
await withGuard(ctx, "upsert");
|
|
708
776
|
const doUpsert = async (inputData) => {
|
|
709
777
|
const inputId = inputData[idField];
|
|
@@ -739,6 +807,131 @@ function createCrudRouter(config) {
|
|
|
739
807
|
next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
|
|
740
808
|
});
|
|
741
809
|
return doUpsert(input);
|
|
810
|
+
}),
|
|
811
|
+
export: resolved.procedureFactory("export").input(exportInputSchema).query(async ({ ctx, input }) => {
|
|
812
|
+
await withGuard(ctx, "export");
|
|
813
|
+
const doExport = async (exportInput$1) => {
|
|
814
|
+
const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
|
|
815
|
+
const validatedFilters = exportInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
|
|
816
|
+
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
817
|
+
table,
|
|
818
|
+
filters: validatedFilters,
|
|
819
|
+
joinOperator: exportInput$1.joinOperator ?? "and"
|
|
820
|
+
}) : void 0);
|
|
821
|
+
let query = ctx.db.select().from(table).$dynamic();
|
|
822
|
+
if (where) query = query.where(where);
|
|
823
|
+
if (exportInput$1.sort?.length) {
|
|
824
|
+
const sortField = exportInput$1.sort[0];
|
|
825
|
+
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
826
|
+
const column = table[sortField.id];
|
|
827
|
+
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const data = await query.limit(effectiveLimit);
|
|
831
|
+
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
832
|
+
if (where) countQuery = countQuery.where(where);
|
|
833
|
+
const total = (await countQuery)[0]?.count ?? 0;
|
|
834
|
+
return {
|
|
835
|
+
data,
|
|
836
|
+
total,
|
|
837
|
+
hasMore: total > data.length
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
const exportInput = input;
|
|
841
|
+
if (m.export) return m.export({
|
|
842
|
+
ctx,
|
|
843
|
+
input: exportInput,
|
|
844
|
+
next: async (modifiedInput) => doExport(modifiedInput ?? exportInput)
|
|
845
|
+
});
|
|
846
|
+
return doExport(exportInput);
|
|
847
|
+
}),
|
|
848
|
+
createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
|
|
849
|
+
await withGuard(ctx, "createMany");
|
|
850
|
+
const doCreateMany = async (items) => {
|
|
851
|
+
const injectData = resolved.getInject(ctx, "create");
|
|
852
|
+
const values = items.map((item) => ({
|
|
853
|
+
...item,
|
|
854
|
+
...injectData
|
|
855
|
+
}));
|
|
856
|
+
const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
857
|
+
return {
|
|
858
|
+
created,
|
|
859
|
+
count: created.length
|
|
860
|
+
};
|
|
861
|
+
};
|
|
862
|
+
if (m.createMany) return m.createMany({
|
|
863
|
+
ctx,
|
|
864
|
+
input,
|
|
865
|
+
next: async (modifiedInput) => doCreateMany(modifiedInput ?? input)
|
|
866
|
+
});
|
|
867
|
+
return doCreateMany(input);
|
|
868
|
+
}),
|
|
869
|
+
import: resolved.procedureFactory("import").input(importInputSchema).mutation(async ({ ctx, input }) => {
|
|
870
|
+
await withGuard(ctx, "import");
|
|
871
|
+
const doImport = async (importData$1) => {
|
|
872
|
+
const validRows = [];
|
|
873
|
+
const failed = [];
|
|
874
|
+
for (let i = 0; i < importData$1.rows.length; i++) {
|
|
875
|
+
const row = importData$1.rows[i];
|
|
876
|
+
const result = schema.safeParse(row);
|
|
877
|
+
if (result.success) validRows.push(result.data);
|
|
878
|
+
else {
|
|
879
|
+
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
880
|
+
failed.push({
|
|
881
|
+
row: i,
|
|
882
|
+
errors
|
|
883
|
+
});
|
|
884
|
+
if (importData$1.onConflict === "error") return {
|
|
885
|
+
success: 0,
|
|
886
|
+
updated: 0,
|
|
887
|
+
skipped: 0,
|
|
888
|
+
failed
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (validRows.length === 0) return {
|
|
893
|
+
success: 0,
|
|
894
|
+
updated: 0,
|
|
895
|
+
skipped: 0,
|
|
896
|
+
failed
|
|
897
|
+
};
|
|
898
|
+
const injectData = resolved.getInject(ctx, "create");
|
|
899
|
+
const values = validRows.map((row) => ({
|
|
900
|
+
...row,
|
|
901
|
+
...injectData
|
|
902
|
+
}));
|
|
903
|
+
let insertedCount = 0;
|
|
904
|
+
let updatedCount = 0;
|
|
905
|
+
if (importData$1.onConflict === "upsert") {
|
|
906
|
+
const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where((0, drizzle_orm.inArray)(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
|
|
907
|
+
const updateFields = Object.keys(validRows[0]);
|
|
908
|
+
const setClause = { ...resolved.getInject(ctx, "update") };
|
|
909
|
+
for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
|
|
910
|
+
const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
|
|
911
|
+
target: getIdColumn(),
|
|
912
|
+
set: setClause
|
|
913
|
+
}).returning({ id: getIdColumn() });
|
|
914
|
+
updatedCount = Math.min(existingCount, results.length);
|
|
915
|
+
insertedCount = results.length - updatedCount;
|
|
916
|
+
} else {
|
|
917
|
+
insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
|
|
918
|
+
updatedCount = 0;
|
|
919
|
+
}
|
|
920
|
+
const skipped = validRows.length - insertedCount - updatedCount;
|
|
921
|
+
return {
|
|
922
|
+
success: insertedCount,
|
|
923
|
+
updated: updatedCount,
|
|
924
|
+
skipped,
|
|
925
|
+
failed
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
const importData = input;
|
|
929
|
+
if (m.import) return m.import({
|
|
930
|
+
ctx,
|
|
931
|
+
input: importData,
|
|
932
|
+
next: async (modifiedInput) => doImport(modifiedInput ?? importData)
|
|
933
|
+
});
|
|
934
|
+
return doImport(importData);
|
|
742
935
|
})
|
|
743
936
|
};
|
|
744
937
|
const crudRouter = router(procedures);
|