@wordrhyme/auto-crud-server 0.6.0 → 0.6.2
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 +37 -1
- package/dist/index.cjs +89 -77
- package/dist/index.d.cts +129 -194
- package/dist/index.d.ts +129 -194
- package/dist/index.js +91 -72
- package/package.json +5 -3
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
|
## 🎯 高级过滤
|
package/dist/index.cjs
CHANGED
|
@@ -474,6 +474,14 @@ 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
486
|
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
|
|
479
487
|
const resolved = resolveConfig(config);
|
|
@@ -485,7 +493,7 @@ function createCrudRouter(config) {
|
|
|
485
493
|
const conditions = [];
|
|
486
494
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
487
495
|
if (scopeCondition) conditions.push(scopeCondition);
|
|
488
|
-
if (softDelete
|
|
496
|
+
if (softDelete) {
|
|
489
497
|
const col = getSoftDeleteColumn();
|
|
490
498
|
if (col) conditions.push((0, drizzle_orm.isNull)(col));
|
|
491
499
|
}
|
|
@@ -496,12 +504,27 @@ function createCrudRouter(config) {
|
|
|
496
504
|
};
|
|
497
505
|
const withGuard = async (ctx, operation) => {
|
|
498
506
|
if (resolved.guard) {
|
|
499
|
-
if (
|
|
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 __trpc_server.TRPCError({
|
|
511
|
+
code: "FORBIDDEN",
|
|
512
|
+
message: "Forbidden: upsert requires both create and update permissions"
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!await resolved.guard(ctx, operation)) throw new __trpc_server.TRPCError({
|
|
517
|
+
code: "FORBIDDEN",
|
|
518
|
+
message: `Forbidden: ${operation} not allowed`
|
|
519
|
+
});
|
|
500
520
|
}
|
|
501
521
|
};
|
|
502
522
|
const withAuthorize = async (ctx, resource, operation) => {
|
|
503
523
|
if (!resource) return;
|
|
504
|
-
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new
|
|
524
|
+
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new __trpc_server.TRPCError({
|
|
525
|
+
code: "FORBIDDEN",
|
|
526
|
+
message: `Forbidden: Cannot ${operation} this resource`
|
|
527
|
+
});
|
|
505
528
|
};
|
|
506
529
|
const procedures = {
|
|
507
530
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
@@ -543,7 +566,7 @@ function createCrudRouter(config) {
|
|
|
543
566
|
});
|
|
544
567
|
return doList(listInput);
|
|
545
568
|
}),
|
|
546
|
-
|
|
569
|
+
get: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
|
|
547
570
|
await withGuard(ctx, "get");
|
|
548
571
|
const doGet = async () => {
|
|
549
572
|
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
|
|
@@ -584,7 +607,10 @@ function createCrudRouter(config) {
|
|
|
584
607
|
const where = buildWhere(ctx, "update", (0, drizzle_orm.eq)(getIdColumn(), input.id));
|
|
585
608
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
586
609
|
await withAuthorize(ctx, existing, "update");
|
|
587
|
-
if (!existing) throw new
|
|
610
|
+
if (!existing) throw new __trpc_server.TRPCError({
|
|
611
|
+
code: "NOT_FOUND",
|
|
612
|
+
message: "Resource not found or access denied"
|
|
613
|
+
});
|
|
588
614
|
const doUpdate = async (updateData) => {
|
|
589
615
|
const injectData = resolved.getInject(ctx, "update");
|
|
590
616
|
const data = {
|
|
@@ -608,7 +634,10 @@ function createCrudRouter(config) {
|
|
|
608
634
|
const where = buildWhere(ctx, "delete", (0, drizzle_orm.eq)(getIdColumn(), input));
|
|
609
635
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
610
636
|
await withAuthorize(ctx, existing, "delete");
|
|
611
|
-
if (!existing) throw new
|
|
637
|
+
if (!existing) throw new __trpc_server.TRPCError({
|
|
638
|
+
code: "NOT_FOUND",
|
|
639
|
+
message: "Resource not found or access denied"
|
|
640
|
+
});
|
|
612
641
|
const doDelete = async () => {
|
|
613
642
|
let deleted;
|
|
614
643
|
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
@@ -627,8 +656,8 @@ function createCrudRouter(config) {
|
|
|
627
656
|
await withGuard(ctx, "deleteMany");
|
|
628
657
|
const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
|
|
629
658
|
const doDeleteMany = async () => {
|
|
630
|
-
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
631
|
-
else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
659
|
+
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
|
|
660
|
+
else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
|
|
632
661
|
};
|
|
633
662
|
if (m.deleteMany) return m.deleteMany({
|
|
634
663
|
ctx,
|
|
@@ -649,7 +678,7 @@ function createCrudRouter(config) {
|
|
|
649
678
|
...updateData,
|
|
650
679
|
...injectData
|
|
651
680
|
};
|
|
652
|
-
return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
681
|
+
return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
|
|
653
682
|
};
|
|
654
683
|
if (m.updateMany) return m.updateMany({
|
|
655
684
|
ctx,
|
|
@@ -658,6 +687,43 @@ function createCrudRouter(config) {
|
|
|
658
687
|
next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
|
|
659
688
|
});
|
|
660
689
|
return doUpdateMany(input.data);
|
|
690
|
+
}),
|
|
691
|
+
upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
692
|
+
await withGuard(ctx, "upsert");
|
|
693
|
+
const doUpsert = async (inputData) => {
|
|
694
|
+
const inputId = inputData[idField];
|
|
695
|
+
let isNew = true;
|
|
696
|
+
let existing = null;
|
|
697
|
+
if (inputId) {
|
|
698
|
+
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
|
|
699
|
+
const [found] = await ctx.db.select().from(table).where(where);
|
|
700
|
+
existing = found;
|
|
701
|
+
isNew = !existing;
|
|
702
|
+
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
703
|
+
}
|
|
704
|
+
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
705
|
+
const data = {
|
|
706
|
+
...inputData,
|
|
707
|
+
...injectData
|
|
708
|
+
};
|
|
709
|
+
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
710
|
+
target: getIdColumn(),
|
|
711
|
+
set: {
|
|
712
|
+
...inputData,
|
|
713
|
+
...resolved.getInject(ctx, "update")
|
|
714
|
+
}
|
|
715
|
+
}).returning();
|
|
716
|
+
return {
|
|
717
|
+
data: result,
|
|
718
|
+
isNew
|
|
719
|
+
};
|
|
720
|
+
};
|
|
721
|
+
if (m.upsert) return m.upsert({
|
|
722
|
+
ctx,
|
|
723
|
+
input,
|
|
724
|
+
next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
|
|
725
|
+
});
|
|
726
|
+
return doUpsert(input);
|
|
661
727
|
})
|
|
662
728
|
};
|
|
663
729
|
const crudRouter = router(procedures);
|
|
@@ -667,6 +733,10 @@ function createCrudRouter(config) {
|
|
|
667
733
|
//#endregion
|
|
668
734
|
//#region src/lib/middleware-helpers.ts
|
|
669
735
|
/**
|
|
736
|
+
* Middleware 便捷工具函数
|
|
737
|
+
* 简化常见场景的 middleware 创建
|
|
738
|
+
*/
|
|
739
|
+
/**
|
|
670
740
|
* 创建 after-only 的 middleware(最常见场景)
|
|
671
741
|
* 在操作完成后执行副作用,不修改返回值
|
|
672
742
|
*
|
|
@@ -679,28 +749,13 @@ function createCrudRouter(config) {
|
|
|
679
749
|
* }
|
|
680
750
|
*/
|
|
681
751
|
function afterMiddleware(fn) {
|
|
682
|
-
return async (
|
|
683
|
-
const result = await next();
|
|
684
|
-
await fn(ctx, result);
|
|
752
|
+
return async (params) => {
|
|
753
|
+
const result = await params.next();
|
|
754
|
+
await fn(params.ctx, result);
|
|
685
755
|
return result;
|
|
686
756
|
};
|
|
687
757
|
}
|
|
688
758
|
/**
|
|
689
|
-
* 创建 after-only 的 middleware,可修改返回值
|
|
690
|
-
*
|
|
691
|
-
* @example
|
|
692
|
-
* middleware: {
|
|
693
|
-
* get: afterMiddlewareTransform(async (ctx, result) => {
|
|
694
|
-
* return { ...result, computed: calculateSomething(result) };
|
|
695
|
-
* }),
|
|
696
|
-
* }
|
|
697
|
-
*/
|
|
698
|
-
function afterMiddlewareTransform(fn) {
|
|
699
|
-
return async ({ ctx, next }) => {
|
|
700
|
-
return fn(ctx, await next());
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
759
|
* 创建 before-only 的 middleware
|
|
705
760
|
* 在操作执行前修改输入数据
|
|
706
761
|
*
|
|
@@ -712,8 +767,9 @@ function afterMiddlewareTransform(fn) {
|
|
|
712
767
|
* }
|
|
713
768
|
*/
|
|
714
769
|
function beforeMiddleware(fn) {
|
|
715
|
-
return async (
|
|
716
|
-
|
|
770
|
+
return async (params) => {
|
|
771
|
+
const modifiedInput = await fn(params.ctx, params.input);
|
|
772
|
+
return params.next(modifiedInput);
|
|
717
773
|
};
|
|
718
774
|
}
|
|
719
775
|
/**
|
|
@@ -730,69 +786,25 @@ function beforeMiddleware(fn) {
|
|
|
730
786
|
function composeMiddleware(...middlewares) {
|
|
731
787
|
return async (params) => {
|
|
732
788
|
let index = 0;
|
|
733
|
-
const executeNext = async (
|
|
734
|
-
if (index >= middlewares.length) return params.next(
|
|
789
|
+
const executeNext = async (...args) => {
|
|
790
|
+
if (index >= middlewares.length) return params.next(...args);
|
|
735
791
|
const currentMiddleware = middlewares[index++];
|
|
736
792
|
return currentMiddleware({
|
|
737
|
-
|
|
738
|
-
input,
|
|
793
|
+
...params,
|
|
739
794
|
next: executeNext
|
|
740
795
|
});
|
|
741
796
|
};
|
|
742
|
-
return executeNext(
|
|
797
|
+
return executeNext();
|
|
743
798
|
};
|
|
744
799
|
}
|
|
745
|
-
/**
|
|
746
|
-
* 创建 list 操作的 after middleware
|
|
747
|
-
*/
|
|
748
|
-
function afterList(fn) {
|
|
749
|
-
return afterMiddleware(fn);
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* 创建 list 操作的 before middleware
|
|
753
|
-
*/
|
|
754
|
-
function beforeList(fn) {
|
|
755
|
-
return beforeMiddleware(fn);
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* 创建 create 操作的 after middleware
|
|
759
|
-
*/
|
|
760
|
-
function afterCreate(fn) {
|
|
761
|
-
return afterMiddleware(fn);
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* 创建 create 操作的 before middleware
|
|
765
|
-
*/
|
|
766
|
-
function beforeCreate(fn) {
|
|
767
|
-
return beforeMiddleware(fn);
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* 创建 update 操作的 after middleware
|
|
771
|
-
*/
|
|
772
|
-
function afterUpdate(fn) {
|
|
773
|
-
return afterMiddleware(fn);
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* 创建 delete 操作的 after middleware
|
|
777
|
-
*/
|
|
778
|
-
function afterDelete(fn) {
|
|
779
|
-
return afterMiddleware(fn);
|
|
780
|
-
}
|
|
781
800
|
|
|
782
801
|
//#endregion
|
|
783
802
|
//#region src/routers/index.ts
|
|
784
803
|
const appRouter = router({});
|
|
785
804
|
|
|
786
805
|
//#endregion
|
|
787
|
-
exports.afterCreate = afterCreate;
|
|
788
|
-
exports.afterDelete = afterDelete;
|
|
789
|
-
exports.afterList = afterList;
|
|
790
806
|
exports.afterMiddleware = afterMiddleware;
|
|
791
|
-
exports.afterMiddlewareTransform = afterMiddlewareTransform;
|
|
792
|
-
exports.afterUpdate = afterUpdate;
|
|
793
807
|
exports.appRouter = appRouter;
|
|
794
|
-
exports.beforeCreate = beforeCreate;
|
|
795
|
-
exports.beforeList = beforeList;
|
|
796
808
|
exports.beforeMiddleware = beforeMiddleware;
|
|
797
809
|
exports.composeMiddleware = composeMiddleware;
|
|
798
810
|
exports.createCrudRouter = createCrudRouter;
|