@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/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
|
|
3
|
-
import { initTRPC } from "@trpc/server";
|
|
3
|
+
import { TRPCError, initTRPC } from "@trpc/server";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { addDays, endOfDay, startOfDay } from "date-fns";
|
|
6
6
|
|
|
@@ -446,6 +446,14 @@ function validateColumn(columnId, allowedColumns) {
|
|
|
446
446
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
447
447
|
return allowedColumns.includes(columnId);
|
|
448
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* 创建 CRUD Router
|
|
451
|
+
*
|
|
452
|
+
* @typeParam TContext - 上下文类型(包含 db)
|
|
453
|
+
* @typeParam TSelect - 查询返回类型(从 selectSchema 或 insertSchema 推断)
|
|
454
|
+
* @typeParam TInsert - 创建输入类型(从 insertSchema 推断)
|
|
455
|
+
* @typeParam TUpdate - 更新输入类型(从 updateSchema 推断)
|
|
456
|
+
*/
|
|
449
457
|
function createCrudRouter(config) {
|
|
450
458
|
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
|
|
451
459
|
const resolved = resolveConfig(config);
|
|
@@ -457,7 +465,7 @@ function createCrudRouter(config) {
|
|
|
457
465
|
const conditions = [];
|
|
458
466
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
459
467
|
if (scopeCondition) conditions.push(scopeCondition);
|
|
460
|
-
if (softDelete
|
|
468
|
+
if (softDelete) {
|
|
461
469
|
const col = getSoftDeleteColumn();
|
|
462
470
|
if (col) conditions.push(isNull(col));
|
|
463
471
|
}
|
|
@@ -468,12 +476,27 @@ function createCrudRouter(config) {
|
|
|
468
476
|
};
|
|
469
477
|
const withGuard = async (ctx, operation) => {
|
|
470
478
|
if (resolved.guard) {
|
|
471
|
-
if (
|
|
479
|
+
if (operation === "upsert") {
|
|
480
|
+
const canCreate = await resolved.guard(ctx, "create");
|
|
481
|
+
const canUpdate = await resolved.guard(ctx, "update");
|
|
482
|
+
if (!canCreate || !canUpdate) throw new TRPCError({
|
|
483
|
+
code: "FORBIDDEN",
|
|
484
|
+
message: "Forbidden: upsert requires both create and update permissions"
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!await resolved.guard(ctx, operation)) throw new TRPCError({
|
|
489
|
+
code: "FORBIDDEN",
|
|
490
|
+
message: `Forbidden: ${operation} not allowed`
|
|
491
|
+
});
|
|
472
492
|
}
|
|
473
493
|
};
|
|
474
494
|
const withAuthorize = async (ctx, resource, operation) => {
|
|
475
495
|
if (!resource) return;
|
|
476
|
-
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new
|
|
496
|
+
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new TRPCError({
|
|
497
|
+
code: "FORBIDDEN",
|
|
498
|
+
message: `Forbidden: Cannot ${operation} this resource`
|
|
499
|
+
});
|
|
477
500
|
};
|
|
478
501
|
const procedures = {
|
|
479
502
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
@@ -515,7 +538,7 @@ function createCrudRouter(config) {
|
|
|
515
538
|
});
|
|
516
539
|
return doList(listInput);
|
|
517
540
|
}),
|
|
518
|
-
|
|
541
|
+
get: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
|
|
519
542
|
await withGuard(ctx, "get");
|
|
520
543
|
const doGet = async () => {
|
|
521
544
|
const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
|
|
@@ -556,7 +579,10 @@ function createCrudRouter(config) {
|
|
|
556
579
|
const where = buildWhere(ctx, "update", eq(getIdColumn(), input.id));
|
|
557
580
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
558
581
|
await withAuthorize(ctx, existing, "update");
|
|
559
|
-
if (!existing) throw new
|
|
582
|
+
if (!existing) throw new TRPCError({
|
|
583
|
+
code: "NOT_FOUND",
|
|
584
|
+
message: "Resource not found or access denied"
|
|
585
|
+
});
|
|
560
586
|
const doUpdate = async (updateData) => {
|
|
561
587
|
const injectData = resolved.getInject(ctx, "update");
|
|
562
588
|
const data = {
|
|
@@ -580,7 +606,10 @@ function createCrudRouter(config) {
|
|
|
580
606
|
const where = buildWhere(ctx, "delete", eq(getIdColumn(), input));
|
|
581
607
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
582
608
|
await withAuthorize(ctx, existing, "delete");
|
|
583
|
-
if (!existing) throw new
|
|
609
|
+
if (!existing) throw new TRPCError({
|
|
610
|
+
code: "NOT_FOUND",
|
|
611
|
+
message: "Resource not found or access denied"
|
|
612
|
+
});
|
|
584
613
|
const doDelete = async () => {
|
|
585
614
|
let deleted;
|
|
586
615
|
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
@@ -599,8 +628,8 @@ function createCrudRouter(config) {
|
|
|
599
628
|
await withGuard(ctx, "deleteMany");
|
|
600
629
|
const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
|
|
601
630
|
const doDeleteMany = async () => {
|
|
602
|
-
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
603
|
-
else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
631
|
+
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
|
|
632
|
+
else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
|
|
604
633
|
};
|
|
605
634
|
if (m.deleteMany) return m.deleteMany({
|
|
606
635
|
ctx,
|
|
@@ -621,7 +650,7 @@ function createCrudRouter(config) {
|
|
|
621
650
|
...updateData,
|
|
622
651
|
...injectData
|
|
623
652
|
};
|
|
624
|
-
return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
653
|
+
return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
|
|
625
654
|
};
|
|
626
655
|
if (m.updateMany) return m.updateMany({
|
|
627
656
|
ctx,
|
|
@@ -630,6 +659,43 @@ function createCrudRouter(config) {
|
|
|
630
659
|
next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
|
|
631
660
|
});
|
|
632
661
|
return doUpdateMany(input.data);
|
|
662
|
+
}),
|
|
663
|
+
upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
664
|
+
await withGuard(ctx, "upsert");
|
|
665
|
+
const doUpsert = async (inputData) => {
|
|
666
|
+
const inputId = inputData[idField];
|
|
667
|
+
let isNew = true;
|
|
668
|
+
let existing = null;
|
|
669
|
+
if (inputId) {
|
|
670
|
+
const where = buildWhere(ctx, "get", eq(getIdColumn(), inputId));
|
|
671
|
+
const [found] = await ctx.db.select().from(table).where(where);
|
|
672
|
+
existing = found;
|
|
673
|
+
isNew = !existing;
|
|
674
|
+
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
675
|
+
}
|
|
676
|
+
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
677
|
+
const data = {
|
|
678
|
+
...inputData,
|
|
679
|
+
...injectData
|
|
680
|
+
};
|
|
681
|
+
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
682
|
+
target: getIdColumn(),
|
|
683
|
+
set: {
|
|
684
|
+
...inputData,
|
|
685
|
+
...resolved.getInject(ctx, "update")
|
|
686
|
+
}
|
|
687
|
+
}).returning();
|
|
688
|
+
return {
|
|
689
|
+
data: result,
|
|
690
|
+
isNew
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
if (m.upsert) return m.upsert({
|
|
694
|
+
ctx,
|
|
695
|
+
input,
|
|
696
|
+
next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
|
|
697
|
+
});
|
|
698
|
+
return doUpsert(input);
|
|
633
699
|
})
|
|
634
700
|
};
|
|
635
701
|
const crudRouter = router(procedures);
|
|
@@ -639,6 +705,10 @@ function createCrudRouter(config) {
|
|
|
639
705
|
//#endregion
|
|
640
706
|
//#region src/lib/middleware-helpers.ts
|
|
641
707
|
/**
|
|
708
|
+
* Middleware 便捷工具函数
|
|
709
|
+
* 简化常见场景的 middleware 创建
|
|
710
|
+
*/
|
|
711
|
+
/**
|
|
642
712
|
* 创建 after-only 的 middleware(最常见场景)
|
|
643
713
|
* 在操作完成后执行副作用,不修改返回值
|
|
644
714
|
*
|
|
@@ -651,28 +721,13 @@ function createCrudRouter(config) {
|
|
|
651
721
|
* }
|
|
652
722
|
*/
|
|
653
723
|
function afterMiddleware(fn) {
|
|
654
|
-
return async (
|
|
655
|
-
const result = await next();
|
|
656
|
-
await fn(ctx, result);
|
|
724
|
+
return async (params) => {
|
|
725
|
+
const result = await params.next();
|
|
726
|
+
await fn(params.ctx, result);
|
|
657
727
|
return result;
|
|
658
728
|
};
|
|
659
729
|
}
|
|
660
730
|
/**
|
|
661
|
-
* 创建 after-only 的 middleware,可修改返回值
|
|
662
|
-
*
|
|
663
|
-
* @example
|
|
664
|
-
* middleware: {
|
|
665
|
-
* get: afterMiddlewareTransform(async (ctx, result) => {
|
|
666
|
-
* return { ...result, computed: calculateSomething(result) };
|
|
667
|
-
* }),
|
|
668
|
-
* }
|
|
669
|
-
*/
|
|
670
|
-
function afterMiddlewareTransform(fn) {
|
|
671
|
-
return async ({ ctx, next }) => {
|
|
672
|
-
return fn(ctx, await next());
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
731
|
* 创建 before-only 的 middleware
|
|
677
732
|
* 在操作执行前修改输入数据
|
|
678
733
|
*
|
|
@@ -684,8 +739,9 @@ function afterMiddlewareTransform(fn) {
|
|
|
684
739
|
* }
|
|
685
740
|
*/
|
|
686
741
|
function beforeMiddleware(fn) {
|
|
687
|
-
return async (
|
|
688
|
-
|
|
742
|
+
return async (params) => {
|
|
743
|
+
const modifiedInput = await fn(params.ctx, params.input);
|
|
744
|
+
return params.next(modifiedInput);
|
|
689
745
|
};
|
|
690
746
|
}
|
|
691
747
|
/**
|
|
@@ -702,58 +758,21 @@ function beforeMiddleware(fn) {
|
|
|
702
758
|
function composeMiddleware(...middlewares) {
|
|
703
759
|
return async (params) => {
|
|
704
760
|
let index = 0;
|
|
705
|
-
const executeNext = async (
|
|
706
|
-
if (index >= middlewares.length) return params.next(
|
|
761
|
+
const executeNext = async (...args) => {
|
|
762
|
+
if (index >= middlewares.length) return params.next(...args);
|
|
707
763
|
const currentMiddleware = middlewares[index++];
|
|
708
764
|
return currentMiddleware({
|
|
709
|
-
|
|
710
|
-
input,
|
|
765
|
+
...params,
|
|
711
766
|
next: executeNext
|
|
712
767
|
});
|
|
713
768
|
};
|
|
714
|
-
return executeNext(
|
|
769
|
+
return executeNext();
|
|
715
770
|
};
|
|
716
771
|
}
|
|
717
|
-
/**
|
|
718
|
-
* 创建 list 操作的 after middleware
|
|
719
|
-
*/
|
|
720
|
-
function afterList(fn) {
|
|
721
|
-
return afterMiddleware(fn);
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* 创建 list 操作的 before middleware
|
|
725
|
-
*/
|
|
726
|
-
function beforeList(fn) {
|
|
727
|
-
return beforeMiddleware(fn);
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* 创建 create 操作的 after middleware
|
|
731
|
-
*/
|
|
732
|
-
function afterCreate(fn) {
|
|
733
|
-
return afterMiddleware(fn);
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* 创建 create 操作的 before middleware
|
|
737
|
-
*/
|
|
738
|
-
function beforeCreate(fn) {
|
|
739
|
-
return beforeMiddleware(fn);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* 创建 update 操作的 after middleware
|
|
743
|
-
*/
|
|
744
|
-
function afterUpdate(fn) {
|
|
745
|
-
return afterMiddleware(fn);
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* 创建 delete 操作的 after middleware
|
|
749
|
-
*/
|
|
750
|
-
function afterDelete(fn) {
|
|
751
|
-
return afterMiddleware(fn);
|
|
752
|
-
}
|
|
753
772
|
|
|
754
773
|
//#endregion
|
|
755
774
|
//#region src/routers/index.ts
|
|
756
775
|
const appRouter = router({});
|
|
757
776
|
|
|
758
777
|
//#endregion
|
|
759
|
-
export {
|
|
778
|
+
export { afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
"superjson": "^2.2.6",
|
|
46
46
|
"tsdown": "^0.15.12",
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.0.18",
|
|
48
49
|
"zod": "^4.1.13",
|
|
49
|
-
"@internal/eslint-config": "0.3.0",
|
|
50
50
|
"@internal/prettier-config": "0.0.1",
|
|
51
51
|
"@internal/tsconfig": "0.1.0",
|
|
52
52
|
"@internal/tsdown-config": "0.1.0",
|
|
53
|
-
"@internal/vitest-config": "0.1.0"
|
|
53
|
+
"@internal/vitest-config": "0.1.0",
|
|
54
|
+
"@internal/eslint-config": "0.3.0"
|
|
54
55
|
},
|
|
55
56
|
"prettier": "@internal/prettier-config",
|
|
56
57
|
"scripts": {
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"build:watch": "tsdown --watch",
|
|
61
62
|
"test": "vitest --run --passWithNoTests",
|
|
62
63
|
"test:watch": "vitest --watch",
|
|
64
|
+
"test:coverage": "vitest --run --coverage",
|
|
63
65
|
"typecheck": "tsc --noEmit",
|
|
64
66
|
"lint": "eslint",
|
|
65
67
|
"format": "prettier --check . --ignore-path ../../.gitignore"
|