@wordrhyme/auto-crud-server 0.4.0 → 0.6.1
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 -2
- package/dist/index.cjs +232 -96
- package/dist/index.d.cts +215 -120
- package/dist/index.d.ts +208 -113
- package/dist/index.js +230 -97
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ export { handler as GET, handler as POST };
|
|
|
136
136
|
|
|
137
137
|
## 📖 自动生成的路由
|
|
138
138
|
|
|
139
|
-
`createCrudRouter` 会自动生成以下
|
|
139
|
+
`createCrudRouter` 会自动生成以下 8 个路由:
|
|
140
140
|
|
|
141
141
|
### 1. `list` - 列表查询
|
|
142
142
|
|
|
@@ -299,6 +299,42 @@ await trpc.tasks.updateMany({
|
|
|
299
299
|
});
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
+
### 8. `upsert` - 存在则更新,不存在则创建
|
|
303
|
+
|
|
304
|
+
**输入**:
|
|
305
|
+
```typescript
|
|
306
|
+
Omit<Task, "createdAt" | "updatedAt"> // 需要包含 id
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**输出**:
|
|
310
|
+
```typescript
|
|
311
|
+
{
|
|
312
|
+
data: Task; // 创建或更新后的记录
|
|
313
|
+
isNew: boolean; // true = 新建, false = 更新
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**示例**:
|
|
318
|
+
```typescript
|
|
319
|
+
// 如果 id="123" 存在则更新,不存在则创建
|
|
320
|
+
const { data, isNew } = await trpc.tasks.upsert({
|
|
321
|
+
id: "123",
|
|
322
|
+
title: "My Task",
|
|
323
|
+
status: "todo",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (isNew) {
|
|
327
|
+
console.log("Created new task");
|
|
328
|
+
} else {
|
|
329
|
+
console.log("Updated existing task");
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**适用场景**:
|
|
334
|
+
- 同步外部数据
|
|
335
|
+
- 幂等导入
|
|
336
|
+
- 配置项更新
|
|
337
|
+
|
|
302
338
|
---
|
|
303
339
|
|
|
304
340
|
## 🎯 高级过滤
|
|
@@ -608,6 +644,110 @@ export const tasksRouter = router({
|
|
|
608
644
|
|
|
609
645
|
---
|
|
610
646
|
|
|
647
|
+
## 🔄 生命周期中间件
|
|
648
|
+
|
|
649
|
+
使用 `middleware` 配置在 CRUD 操作前后注入自定义逻辑,类似 tRPC middleware 模式。
|
|
650
|
+
|
|
651
|
+
### 完整控制模式
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { createCrudRouter } from "@wordrhyme/auto-crud-server";
|
|
655
|
+
|
|
656
|
+
const tasksRouter = createCrudRouter({
|
|
657
|
+
table: tasks,
|
|
658
|
+
insertSchema: insertTaskSchema,
|
|
659
|
+
updateSchema: updateTaskSchema,
|
|
660
|
+
|
|
661
|
+
// 中间件:完全控制操作流程
|
|
662
|
+
middleware: {
|
|
663
|
+
// 创建:修改输入 + 执行副作用
|
|
664
|
+
create: async ({ ctx, input, next }) => {
|
|
665
|
+
// 1. 修改输入数据
|
|
666
|
+
const data = { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
|
|
667
|
+
|
|
668
|
+
// 2. 执行核心操作
|
|
669
|
+
const result = await next(data);
|
|
670
|
+
|
|
671
|
+
// 3. 执行副作用
|
|
672
|
+
await sendNotification(result);
|
|
673
|
+
await logAudit(ctx.user, "create", result);
|
|
674
|
+
|
|
675
|
+
// 4. 返回结果(可修改)
|
|
676
|
+
return result;
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
// 更新:可访问原记录
|
|
680
|
+
update: async ({ ctx, id, data, existing, next }) => {
|
|
681
|
+
// 检查权限
|
|
682
|
+
if (existing.ownerId !== ctx.user.id) {
|
|
683
|
+
throw new Error("Forbidden");
|
|
684
|
+
}
|
|
685
|
+
return next(data);
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
// 删除:条件拦截
|
|
689
|
+
delete: async ({ ctx, id, existing, next }) => {
|
|
690
|
+
if (existing.status === "locked") {
|
|
691
|
+
throw new Error("Cannot delete locked resource");
|
|
692
|
+
}
|
|
693
|
+
return next();
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### 使用便捷工具函数
|
|
700
|
+
|
|
701
|
+
对于简单场景,使用工具函数简化代码:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
import {
|
|
705
|
+
createCrudRouter,
|
|
706
|
+
afterMiddleware,
|
|
707
|
+
beforeMiddleware,
|
|
708
|
+
afterCreate,
|
|
709
|
+
beforeCreate,
|
|
710
|
+
} from "@wordrhyme/auto-crud-server";
|
|
711
|
+
|
|
712
|
+
const tasksRouter = createCrudRouter({
|
|
713
|
+
table: tasks,
|
|
714
|
+
insertSchema: insertTaskSchema,
|
|
715
|
+
updateSchema: updateTaskSchema,
|
|
716
|
+
|
|
717
|
+
middleware: {
|
|
718
|
+
// 简单副作用:只在操作后执行
|
|
719
|
+
create: afterMiddleware(async (ctx, result) => {
|
|
720
|
+
await sendEmail(result);
|
|
721
|
+
await logAudit(ctx.user, "create", result);
|
|
722
|
+
}),
|
|
723
|
+
|
|
724
|
+
// 修改输入:只在操作前执行
|
|
725
|
+
update: beforeMiddleware(async (ctx, data) => {
|
|
726
|
+
return { ...data, updatedAt: new Date(), updatedBy: ctx.user.id };
|
|
727
|
+
}),
|
|
728
|
+
|
|
729
|
+
// 类型安全的便捷函数
|
|
730
|
+
delete: afterDelete(async (ctx, deleted) => {
|
|
731
|
+
await cleanupRelatedData(deleted.id);
|
|
732
|
+
}),
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### 可用的工具函数
|
|
738
|
+
|
|
739
|
+
| 函数 | 用途 | 示例 |
|
|
740
|
+
|------|------|------|
|
|
741
|
+
| `afterMiddleware(fn)` | 操作后执行副作用 | 日志、通知、审计 |
|
|
742
|
+
| `afterMiddlewareTransform(fn)` | 操作后修改返回值 | 添加计算字段 |
|
|
743
|
+
| `beforeMiddleware(fn)` | 操作前修改输入 | 注入用户ID、生成slug |
|
|
744
|
+
| `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
|
|
745
|
+
| `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
|
|
746
|
+
| `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
|
|
747
|
+
| `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
611
751
|
## 🔌 与其他库集成
|
|
612
752
|
|
|
613
753
|
### 与 Drizzle ORM 集成
|
|
@@ -698,7 +838,26 @@ export const filterVariants = {
|
|
|
698
838
|
```typescript
|
|
699
839
|
// 主要导出
|
|
700
840
|
export { createCrudRouter } from "./routers/_factory";
|
|
701
|
-
export type {
|
|
841
|
+
export type {
|
|
842
|
+
CrudRouterConfig,
|
|
843
|
+
CrudMiddleware,
|
|
844
|
+
ListInput,
|
|
845
|
+
ListResult,
|
|
846
|
+
} from "./types/config";
|
|
847
|
+
|
|
848
|
+
// Middleware 工具函数
|
|
849
|
+
export {
|
|
850
|
+
afterMiddleware,
|
|
851
|
+
afterMiddlewareTransform,
|
|
852
|
+
beforeMiddleware,
|
|
853
|
+
composeMiddleware,
|
|
854
|
+
afterList,
|
|
855
|
+
beforeList,
|
|
856
|
+
afterCreate,
|
|
857
|
+
beforeCreate,
|
|
858
|
+
afterUpdate,
|
|
859
|
+
afterDelete,
|
|
860
|
+
} from "./lib/middleware-helpers";
|
|
702
861
|
|
|
703
862
|
// tRPC 工具
|
|
704
863
|
export { router, publicProcedure } from "./trpc";
|
package/dist/index.cjs
CHANGED
|
@@ -474,11 +474,19 @@ function validateColumn(columnId, allowedColumns) {
|
|
|
474
474
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
475
475
|
return allowedColumns.includes(columnId);
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* 创建 CRUD Router
|
|
479
|
+
*
|
|
480
|
+
* @typeParam TContext - 上下文类型(包含 db)
|
|
481
|
+
* @typeParam TSelect - 查询返回类型(从 selectSchema 或 insertSchema 推断)
|
|
482
|
+
* @typeParam TInsert - 创建输入类型(从 insertSchema 推断)
|
|
483
|
+
* @typeParam TUpdate - 更新输入类型(从 updateSchema 推断)
|
|
484
|
+
*/
|
|
477
485
|
function createCrudRouter(config) {
|
|
478
|
-
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption,
|
|
486
|
+
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
|
|
479
487
|
const resolved = resolveConfig(config);
|
|
480
488
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
481
|
-
const
|
|
489
|
+
const m = middleware;
|
|
482
490
|
const getIdColumn = () => table[idField];
|
|
483
491
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
484
492
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
@@ -496,6 +504,12 @@ function createCrudRouter(config) {
|
|
|
496
504
|
};
|
|
497
505
|
const withGuard = async (ctx, operation) => {
|
|
498
506
|
if (resolved.guard) {
|
|
507
|
+
if (operation === "upsert") {
|
|
508
|
+
const canCreate = await resolved.guard(ctx, "create");
|
|
509
|
+
const canUpdate = await resolved.guard(ctx, "update");
|
|
510
|
+
if (!canCreate || !canUpdate) throw new Error("Forbidden: upsert requires both create and update permissions");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
499
513
|
if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
|
|
500
514
|
}
|
|
501
515
|
};
|
|
@@ -506,72 +520,75 @@ function createCrudRouter(config) {
|
|
|
506
520
|
const procedures = {
|
|
507
521
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
508
522
|
await withGuard(ctx, "list");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
table
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
526
|
-
const column = table[sortField.id];
|
|
527
|
-
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
523
|
+
const doList = async (listInput$1) => {
|
|
524
|
+
const offset = (listInput$1.page - 1) * listInput$1.perPage;
|
|
525
|
+
const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
|
|
526
|
+
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
527
|
+
table,
|
|
528
|
+
filters: validatedFilters,
|
|
529
|
+
joinOperator: listInput$1.joinOperator
|
|
530
|
+
}) : void 0);
|
|
531
|
+
let query = ctx.db.select().from(table).$dynamic();
|
|
532
|
+
if (where) query = query.where(where);
|
|
533
|
+
if (listInput$1.sort?.length) {
|
|
534
|
+
const sortField = listInput$1.sort[0];
|
|
535
|
+
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
536
|
+
const column = table[sortField.id];
|
|
537
|
+
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
538
|
+
}
|
|
528
539
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
+
const data = await query.limit(listInput$1.perPage).offset(offset);
|
|
541
|
+
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
542
|
+
if (where) countQuery = countQuery.where(where);
|
|
543
|
+
const count = (await countQuery)[0]?.count ?? 0;
|
|
544
|
+
return {
|
|
545
|
+
data,
|
|
546
|
+
total: count,
|
|
547
|
+
page: listInput$1.page,
|
|
548
|
+
perPage: listInput$1.perPage,
|
|
549
|
+
pageCount: Math.ceil(count / listInput$1.perPage)
|
|
550
|
+
};
|
|
540
551
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
552
|
+
const listInput = input;
|
|
553
|
+
if (m.list) return m.list({
|
|
554
|
+
ctx,
|
|
555
|
+
input: listInput,
|
|
556
|
+
next: async (modifiedInput) => doList(modifiedInput ?? listInput)
|
|
557
|
+
});
|
|
558
|
+
return doList(listInput);
|
|
546
559
|
}),
|
|
547
|
-
|
|
560
|
+
get: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
|
|
548
561
|
await withGuard(ctx, "get");
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
562
|
+
const doGet = async () => {
|
|
563
|
+
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
|
|
564
|
+
const [item] = await ctx.db.select().from(table).where(where);
|
|
565
|
+
await withAuthorize(ctx, item, "get");
|
|
566
|
+
return item ?? null;
|
|
567
|
+
};
|
|
568
|
+
if (m.get) return m.get({
|
|
569
|
+
ctx,
|
|
570
|
+
id: input,
|
|
571
|
+
next: doGet
|
|
572
|
+
});
|
|
573
|
+
return doGet();
|
|
559
574
|
}),
|
|
560
575
|
create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
561
576
|
await withGuard(ctx, "create");
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
...injectData
|
|
577
|
+
const doCreate = async (inputData) => {
|
|
578
|
+
const injectData = resolved.getInject(ctx, "create");
|
|
579
|
+
const data = {
|
|
580
|
+
...inputData,
|
|
581
|
+
...injectData
|
|
582
|
+
};
|
|
583
|
+
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
584
|
+
return created;
|
|
571
585
|
};
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
586
|
+
if (m.create) return m.create({
|
|
587
|
+
ctx,
|
|
588
|
+
input,
|
|
589
|
+
next: async (modifiedInput) => doCreate(modifiedInput ?? input)
|
|
590
|
+
});
|
|
591
|
+
return doCreate(input);
|
|
575
592
|
}),
|
|
576
593
|
update: resolved.procedureFactory("update").input(zod.z.object({
|
|
577
594
|
id: zod.z.string(),
|
|
@@ -582,19 +599,23 @@ function createCrudRouter(config) {
|
|
|
582
599
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
583
600
|
await withAuthorize(ctx, existing, "update");
|
|
584
601
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
...injectData
|
|
602
|
+
const doUpdate = async (updateData) => {
|
|
603
|
+
const injectData = resolved.getInject(ctx, "update");
|
|
604
|
+
const data = {
|
|
605
|
+
...updateData,
|
|
606
|
+
...injectData
|
|
607
|
+
};
|
|
608
|
+
const [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
609
|
+
return updated;
|
|
594
610
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
611
|
+
if (m.update) return m.update({
|
|
612
|
+
ctx,
|
|
613
|
+
id: input.id,
|
|
614
|
+
data: input.data,
|
|
615
|
+
existing,
|
|
616
|
+
next: async (modifiedData) => doUpdate(modifiedData ?? input.data)
|
|
617
|
+
});
|
|
618
|
+
return doUpdate(input.data);
|
|
598
619
|
}),
|
|
599
620
|
delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
|
|
600
621
|
await withGuard(ctx, "delete");
|
|
@@ -602,54 +623,169 @@ function createCrudRouter(config) {
|
|
|
602
623
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
603
624
|
await withAuthorize(ctx, existing, "delete");
|
|
604
625
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
626
|
+
const doDelete = async () => {
|
|
627
|
+
let deleted;
|
|
628
|
+
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
629
|
+
else [deleted] = await ctx.db.delete(table).where(where).returning();
|
|
630
|
+
return deleted;
|
|
631
|
+
};
|
|
632
|
+
if (m.delete) return m.delete({
|
|
633
|
+
ctx,
|
|
634
|
+
id: input,
|
|
635
|
+
existing,
|
|
636
|
+
next: doDelete
|
|
637
|
+
});
|
|
638
|
+
return doDelete();
|
|
611
639
|
}),
|
|
612
640
|
deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
|
|
613
641
|
await withGuard(ctx, "deleteMany");
|
|
614
|
-
if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
|
|
615
642
|
const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return
|
|
643
|
+
const doDeleteMany = async () => {
|
|
644
|
+
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
645
|
+
else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
646
|
+
};
|
|
647
|
+
if (m.deleteMany) return m.deleteMany({
|
|
648
|
+
ctx,
|
|
649
|
+
ids: input,
|
|
650
|
+
next: doDeleteMany
|
|
651
|
+
});
|
|
652
|
+
return doDeleteMany();
|
|
621
653
|
}),
|
|
622
654
|
updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
|
|
623
655
|
ids: zod.z.array(zod.z.string()).max(maxBatchSize),
|
|
624
656
|
data: updateSchema
|
|
625
657
|
})).mutation(async ({ ctx, input }) => {
|
|
626
658
|
await withGuard(ctx, "updateMany");
|
|
627
|
-
let processedData = input.data;
|
|
628
|
-
if (h.beforeUpdateMany) {
|
|
629
|
-
const result$1 = await h.beforeUpdateMany(ctx, input.ids, processedData);
|
|
630
|
-
if (result$1) processedData = result$1;
|
|
631
|
-
}
|
|
632
659
|
const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
660
|
+
const doUpdateMany = async (updateData) => {
|
|
661
|
+
const injectData = resolved.getInject(ctx, "update");
|
|
662
|
+
const data = {
|
|
663
|
+
...updateData,
|
|
664
|
+
...injectData
|
|
665
|
+
};
|
|
666
|
+
return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
667
|
+
};
|
|
668
|
+
if (m.updateMany) return m.updateMany({
|
|
669
|
+
ctx,
|
|
670
|
+
ids: input.ids,
|
|
671
|
+
data: input.data,
|
|
672
|
+
next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
|
|
673
|
+
});
|
|
674
|
+
return doUpdateMany(input.data);
|
|
675
|
+
}),
|
|
676
|
+
upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
677
|
+
await withGuard(ctx, "upsert");
|
|
678
|
+
const doUpsert = async (inputData) => {
|
|
679
|
+
const injectData = resolved.getInject(ctx, "create");
|
|
680
|
+
const data = {
|
|
681
|
+
...inputData,
|
|
682
|
+
...injectData
|
|
683
|
+
};
|
|
684
|
+
const inputId = inputData[idField];
|
|
685
|
+
let isNew = true;
|
|
686
|
+
if (inputId) {
|
|
687
|
+
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
|
|
688
|
+
const [existing] = await ctx.db.select().from(table).where(where);
|
|
689
|
+
isNew = !existing;
|
|
690
|
+
}
|
|
691
|
+
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
692
|
+
target: getIdColumn(),
|
|
693
|
+
set: data
|
|
694
|
+
}).returning();
|
|
695
|
+
return {
|
|
696
|
+
data: result,
|
|
697
|
+
isNew
|
|
698
|
+
};
|
|
637
699
|
};
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
700
|
+
if (m.upsert) return m.upsert({
|
|
701
|
+
ctx,
|
|
702
|
+
input,
|
|
703
|
+
next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
|
|
704
|
+
});
|
|
705
|
+
return doUpsert(input);
|
|
641
706
|
})
|
|
642
707
|
};
|
|
643
708
|
const crudRouter = router(procedures);
|
|
644
709
|
return Object.assign(crudRouter, { procedures });
|
|
645
710
|
}
|
|
646
711
|
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/lib/middleware-helpers.ts
|
|
714
|
+
/**
|
|
715
|
+
* Middleware 便捷工具函数
|
|
716
|
+
* 简化常见场景的 middleware 创建
|
|
717
|
+
*/
|
|
718
|
+
/**
|
|
719
|
+
* 创建 after-only 的 middleware(最常见场景)
|
|
720
|
+
* 在操作完成后执行副作用,不修改返回值
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* middleware: {
|
|
724
|
+
* create: afterMiddleware(async (ctx, result) => {
|
|
725
|
+
* await sendEmail(result);
|
|
726
|
+
* await logAudit(ctx.user, 'create', result);
|
|
727
|
+
* }),
|
|
728
|
+
* }
|
|
729
|
+
*/
|
|
730
|
+
function afterMiddleware(fn) {
|
|
731
|
+
return async (params) => {
|
|
732
|
+
const result = await params.next();
|
|
733
|
+
await fn(params.ctx, result);
|
|
734
|
+
return result;
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* 创建 before-only 的 middleware
|
|
739
|
+
* 在操作执行前修改输入数据
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* middleware: {
|
|
743
|
+
* create: beforeMiddleware(async (ctx, input) => {
|
|
744
|
+
* return { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
|
|
745
|
+
* }),
|
|
746
|
+
* }
|
|
747
|
+
*/
|
|
748
|
+
function beforeMiddleware(fn) {
|
|
749
|
+
return async (params) => {
|
|
750
|
+
const modifiedInput = await fn(params.ctx, params.input);
|
|
751
|
+
return params.next(modifiedInput);
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* 组合多个 middleware 为一个
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* middleware: {
|
|
759
|
+
* create: composeMiddleware(
|
|
760
|
+
* beforeMiddleware((ctx, input) => ({ ...input, slug: slugify(input.title) })),
|
|
761
|
+
* afterMiddleware((ctx, result) => sendEmail(result)),
|
|
762
|
+
* ),
|
|
763
|
+
* }
|
|
764
|
+
*/
|
|
765
|
+
function composeMiddleware(...middlewares) {
|
|
766
|
+
return async (params) => {
|
|
767
|
+
let index = 0;
|
|
768
|
+
const executeNext = async (...args) => {
|
|
769
|
+
if (index >= middlewares.length) return params.next(...args);
|
|
770
|
+
const currentMiddleware = middlewares[index++];
|
|
771
|
+
return currentMiddleware({
|
|
772
|
+
...params,
|
|
773
|
+
next: executeNext
|
|
774
|
+
});
|
|
775
|
+
};
|
|
776
|
+
return executeNext();
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
647
780
|
//#endregion
|
|
648
781
|
//#region src/routers/index.ts
|
|
649
782
|
const appRouter = router({});
|
|
650
783
|
|
|
651
784
|
//#endregion
|
|
785
|
+
exports.afterMiddleware = afterMiddleware;
|
|
652
786
|
exports.appRouter = appRouter;
|
|
787
|
+
exports.beforeMiddleware = beforeMiddleware;
|
|
788
|
+
exports.composeMiddleware = composeMiddleware;
|
|
653
789
|
exports.createCrudRouter = createCrudRouter;
|
|
654
790
|
exports.publicProcedure = publicProcedure;
|
|
655
791
|
exports.router = router;
|