@wordrhyme/auto-crud-server 0.3.0 → 0.4.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/dist/index.cjs +63 -23
- package/dist/index.d.cts +134 -5
- package/dist/index.d.ts +144 -15
- package/dist/index.js +63 -23
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -475,9 +475,10 @@ function validateColumn(columnId, allowedColumns) {
|
|
|
475
475
|
return allowedColumns.includes(columnId);
|
|
476
476
|
}
|
|
477
477
|
function createCrudRouter(config) {
|
|
478
|
-
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption } = config;
|
|
478
|
+
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, hooks = {} } = config;
|
|
479
479
|
const resolved = resolveConfig(config);
|
|
480
480
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
481
|
+
const h = hooks;
|
|
481
482
|
const getIdColumn = () => table[idField];
|
|
482
483
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
483
484
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
@@ -505,49 +506,71 @@ function createCrudRouter(config) {
|
|
|
505
506
|
const procedures = {
|
|
506
507
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
507
508
|
await withGuard(ctx, "list");
|
|
508
|
-
|
|
509
|
-
|
|
509
|
+
let processedInput = input;
|
|
510
|
+
if (h.beforeList) {
|
|
511
|
+
const result = await h.beforeList(ctx, processedInput);
|
|
512
|
+
if (result) processedInput = result;
|
|
513
|
+
}
|
|
514
|
+
const offset = (processedInput.page - 1) * processedInput.perPage;
|
|
515
|
+
const validatedFilters = processedInput.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
|
|
510
516
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
511
517
|
table,
|
|
512
518
|
filters: validatedFilters,
|
|
513
|
-
joinOperator:
|
|
519
|
+
joinOperator: processedInput.joinOperator
|
|
514
520
|
}) : void 0);
|
|
515
521
|
let query = ctx.db.select().from(table).$dynamic();
|
|
516
522
|
if (where) query = query.where(where);
|
|
517
|
-
if (
|
|
518
|
-
const sortField =
|
|
523
|
+
if (processedInput.sort?.length) {
|
|
524
|
+
const sortField = processedInput.sort[0];
|
|
519
525
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
520
526
|
const column = table[sortField.id];
|
|
521
527
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
522
528
|
}
|
|
523
529
|
}
|
|
524
|
-
const data = await query.limit(
|
|
530
|
+
const data = await query.limit(processedInput.perPage).offset(offset);
|
|
525
531
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
526
532
|
if (where) countQuery = countQuery.where(where);
|
|
527
533
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
528
|
-
|
|
534
|
+
let listResult = {
|
|
529
535
|
data,
|
|
530
536
|
total: count,
|
|
531
|
-
page:
|
|
532
|
-
perPage:
|
|
533
|
-
pageCount: Math.ceil(count /
|
|
537
|
+
page: processedInput.page,
|
|
538
|
+
perPage: processedInput.perPage,
|
|
539
|
+
pageCount: Math.ceil(count / processedInput.perPage)
|
|
534
540
|
};
|
|
541
|
+
if (h.afterList) {
|
|
542
|
+
const result = await h.afterList(ctx, listResult);
|
|
543
|
+
if (result) listResult = result;
|
|
544
|
+
}
|
|
545
|
+
return listResult;
|
|
535
546
|
}),
|
|
536
547
|
getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
|
|
537
548
|
await withGuard(ctx, "get");
|
|
549
|
+
if (h.beforeGet) await h.beforeGet(ctx, input);
|
|
538
550
|
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
|
|
539
551
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
540
552
|
await withAuthorize(ctx, item, "get");
|
|
541
|
-
|
|
553
|
+
let result = item ?? null;
|
|
554
|
+
if (h.afterGet) {
|
|
555
|
+
const hookResult = await h.afterGet(ctx, result);
|
|
556
|
+
if (hookResult !== void 0) result = hookResult;
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
542
559
|
}),
|
|
543
560
|
create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
544
561
|
await withGuard(ctx, "create");
|
|
562
|
+
let processedData = input;
|
|
563
|
+
if (h.beforeCreate) {
|
|
564
|
+
const result = await h.beforeCreate(ctx, processedData);
|
|
565
|
+
if (result) processedData = result;
|
|
566
|
+
}
|
|
545
567
|
const injectData = resolved.getInject(ctx, "create");
|
|
546
568
|
const data = {
|
|
547
|
-
...
|
|
569
|
+
...processedData,
|
|
548
570
|
...injectData
|
|
549
571
|
};
|
|
550
572
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
573
|
+
if (h.afterCreate) await h.afterCreate(ctx, created);
|
|
551
574
|
return created;
|
|
552
575
|
}),
|
|
553
576
|
update: resolved.procedureFactory("update").input(zod.z.object({
|
|
@@ -559,12 +582,18 @@ function createCrudRouter(config) {
|
|
|
559
582
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
560
583
|
await withAuthorize(ctx, existing, "update");
|
|
561
584
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
585
|
+
let processedData = input.data;
|
|
586
|
+
if (h.beforeUpdate) {
|
|
587
|
+
const result = await h.beforeUpdate(ctx, input.id, processedData, existing);
|
|
588
|
+
if (result) processedData = result;
|
|
589
|
+
}
|
|
562
590
|
const injectData = resolved.getInject(ctx, "update");
|
|
563
591
|
const data = {
|
|
564
|
-
...
|
|
592
|
+
...processedData,
|
|
565
593
|
...injectData
|
|
566
594
|
};
|
|
567
595
|
const [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
596
|
+
if (h.afterUpdate) await h.afterUpdate(ctx, updated);
|
|
568
597
|
return updated;
|
|
569
598
|
}),
|
|
570
599
|
delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
|
|
@@ -573,31 +602,42 @@ function createCrudRouter(config) {
|
|
|
573
602
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
574
603
|
await withAuthorize(ctx, existing, "delete");
|
|
575
604
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
605
|
+
if (h.beforeDelete) await h.beforeDelete(ctx, input, existing);
|
|
606
|
+
let deleted;
|
|
607
|
+
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
608
|
+
else [deleted] = await ctx.db.delete(table).where(where).returning();
|
|
609
|
+
if (h.afterDelete) await h.afterDelete(ctx, deleted);
|
|
581
610
|
return deleted;
|
|
582
611
|
}),
|
|
583
612
|
deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
|
|
584
613
|
await withGuard(ctx, "deleteMany");
|
|
614
|
+
if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
|
|
585
615
|
const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
|
|
586
|
-
|
|
587
|
-
|
|
616
|
+
let result;
|
|
617
|
+
if (softDelete) result = { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
618
|
+
else result = { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
619
|
+
if (h.afterDeleteMany) await h.afterDeleteMany(ctx, result);
|
|
620
|
+
return result;
|
|
588
621
|
}),
|
|
589
622
|
updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
|
|
590
623
|
ids: zod.z.array(zod.z.string()).max(maxBatchSize),
|
|
591
624
|
data: updateSchema
|
|
592
625
|
})).mutation(async ({ ctx, input }) => {
|
|
593
626
|
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
|
+
}
|
|
594
632
|
const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
|
|
595
633
|
const injectData = resolved.getInject(ctx, "update");
|
|
596
634
|
const data = {
|
|
597
|
-
...
|
|
635
|
+
...processedData,
|
|
598
636
|
...injectData
|
|
599
637
|
};
|
|
600
|
-
|
|
638
|
+
const result = { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
639
|
+
if (h.afterUpdateMany) await h.afterUpdateMany(ctx, result);
|
|
640
|
+
return result;
|
|
601
641
|
})
|
|
602
642
|
};
|
|
603
643
|
const crudRouter = router(procedures);
|
package/dist/index.d.cts
CHANGED
|
@@ -42,7 +42,121 @@ interface SoftDeleteConfig {
|
|
|
42
42
|
* - SoftDeleteConfig: 完整配置
|
|
43
43
|
*/
|
|
44
44
|
type SoftDeleteOption = string | boolean | SoftDeleteConfig;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* 列表查询输入类型
|
|
47
|
+
*/
|
|
48
|
+
interface ListInput {
|
|
49
|
+
page: number;
|
|
50
|
+
perPage: number;
|
|
51
|
+
sort?: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
desc: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
filters?: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
value: string | string[];
|
|
58
|
+
variant: string;
|
|
59
|
+
operator: string;
|
|
60
|
+
filterId: string;
|
|
61
|
+
}>;
|
|
62
|
+
joinOperator: "and" | "or";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 列表查询结果类型
|
|
66
|
+
*/
|
|
67
|
+
interface ListResult<TSelect> {
|
|
68
|
+
data: TSelect[];
|
|
69
|
+
total: number;
|
|
70
|
+
page: number;
|
|
71
|
+
perPage: number;
|
|
72
|
+
pageCount: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* CRUD 生命周期钩子
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* hooks: {
|
|
79
|
+
* beforeCreate: async (ctx, data) => {
|
|
80
|
+
* // 修改数据
|
|
81
|
+
* return { ...data, slug: slugify(data.title) };
|
|
82
|
+
* },
|
|
83
|
+
* afterCreate: async (ctx, created) => {
|
|
84
|
+
* // 发送通知
|
|
85
|
+
* await sendNotification(created);
|
|
86
|
+
* },
|
|
87
|
+
* }
|
|
88
|
+
*/
|
|
89
|
+
interface CrudHooks<TContext = unknown> {
|
|
90
|
+
/**
|
|
91
|
+
* 列表查询前
|
|
92
|
+
* 可修改查询参数
|
|
93
|
+
*/
|
|
94
|
+
beforeList?: (ctx: TContext, input: ListInput) => ListInput | void | Promise<ListInput | void>;
|
|
95
|
+
/**
|
|
96
|
+
* 列表查询后
|
|
97
|
+
* 可修改返回结果
|
|
98
|
+
*/
|
|
99
|
+
afterList?: <TSelect>(ctx: TContext, result: ListResult<TSelect>) => ListResult<TSelect> | void | Promise<ListResult<TSelect> | void>;
|
|
100
|
+
/**
|
|
101
|
+
* 单条查询前
|
|
102
|
+
*/
|
|
103
|
+
beforeGet?: (ctx: TContext, id: string) => void | Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* 单条查询后
|
|
106
|
+
* 可修改返回结果
|
|
107
|
+
*/
|
|
108
|
+
afterGet?: <TSelect>(ctx: TContext, result: TSelect | null) => TSelect | null | void | Promise<TSelect | null | void>;
|
|
109
|
+
/**
|
|
110
|
+
* 创建前
|
|
111
|
+
* 可修改输入数据,返回新数据或抛出错误阻止创建
|
|
112
|
+
*/
|
|
113
|
+
beforeCreate?: <TInsert>(ctx: TContext, data: TInsert) => TInsert | void | Promise<TInsert | void>;
|
|
114
|
+
/**
|
|
115
|
+
* 创建后
|
|
116
|
+
* 用于副作用(日志、通知、同步等)
|
|
117
|
+
*/
|
|
118
|
+
afterCreate?: <TSelect>(ctx: TContext, created: TSelect) => void | Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* 更新前
|
|
121
|
+
* 可修改输入数据,existing 为更新前的记录
|
|
122
|
+
*/
|
|
123
|
+
beforeUpdate?: <TUpdate, TSelect>(ctx: TContext, id: string, data: TUpdate, existing: TSelect) => TUpdate | void | Promise<TUpdate | void>;
|
|
124
|
+
/**
|
|
125
|
+
* 更新后
|
|
126
|
+
*/
|
|
127
|
+
afterUpdate?: <TSelect>(ctx: TContext, updated: TSelect) => void | Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* 删除前
|
|
130
|
+
* existing 为删除前的记录,可抛出错误阻止删除
|
|
131
|
+
*/
|
|
132
|
+
beforeDelete?: <TSelect>(ctx: TContext, id: string, existing: TSelect) => void | Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* 删除后
|
|
135
|
+
*/
|
|
136
|
+
afterDelete?: <TSelect>(ctx: TContext, deleted: TSelect) => void | Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* 批量删除前
|
|
139
|
+
*/
|
|
140
|
+
beforeDeleteMany?: (ctx: TContext, ids: string[]) => void | Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* 批量删除后
|
|
143
|
+
*/
|
|
144
|
+
afterDeleteMany?: (ctx: TContext, result: {
|
|
145
|
+
deleted: number;
|
|
146
|
+
}) => void | Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* 批量更新前
|
|
149
|
+
* 可修改输入数据
|
|
150
|
+
*/
|
|
151
|
+
beforeUpdateMany?: <TUpdate>(ctx: TContext, ids: string[], data: TUpdate) => TUpdate | void | Promise<TUpdate | void>;
|
|
152
|
+
/**
|
|
153
|
+
* 批量更新后
|
|
154
|
+
*/
|
|
155
|
+
afterUpdateMany?: (ctx: TContext, result: {
|
|
156
|
+
updated: number;
|
|
157
|
+
}) => void | Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
interface CrudRouterConfigBase<TContext = unknown> {
|
|
46
160
|
/** Drizzle 表定义 */
|
|
47
161
|
table: PgTable;
|
|
48
162
|
/** 查询返回 Schema */
|
|
@@ -77,8 +191,23 @@ interface CrudRouterConfigBase {
|
|
|
77
191
|
* softDelete: { column: 'isDeleted', value: () => true }
|
|
78
192
|
*/
|
|
79
193
|
softDelete?: SoftDeleteOption;
|
|
194
|
+
/**
|
|
195
|
+
* 生命周期钩子
|
|
196
|
+
* 在 CRUD 操作前后注入自定义逻辑
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* hooks: {
|
|
200
|
+
* beforeCreate: async (ctx, data) => {
|
|
201
|
+
* return { ...data, slug: slugify(data.title) };
|
|
202
|
+
* },
|
|
203
|
+
* afterCreate: async (ctx, created) => {
|
|
204
|
+
* await sendNotification(created);
|
|
205
|
+
* },
|
|
206
|
+
* }
|
|
207
|
+
*/
|
|
208
|
+
hooks?: CrudHooks<TContext>;
|
|
80
209
|
}
|
|
81
|
-
interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
210
|
+
interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
82
211
|
mode?: "declarative";
|
|
83
212
|
/**
|
|
84
213
|
* 基础 procedure(认证)
|
|
@@ -118,7 +247,7 @@ interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
|
118
247
|
*/
|
|
119
248
|
authorize?: (ctx: TContext, resource: Record<string, unknown>, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
120
249
|
}
|
|
121
|
-
interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
250
|
+
interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
122
251
|
mode: "factory";
|
|
123
252
|
/**
|
|
124
253
|
* Procedure 工厂函数
|
|
@@ -137,7 +266,7 @@ interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
|
137
266
|
*/
|
|
138
267
|
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
139
268
|
}
|
|
140
|
-
interface ProceduresConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
269
|
+
interface ProceduresConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
141
270
|
mode: "procedures";
|
|
142
271
|
/**
|
|
143
272
|
* 按操作指定不同的 procedure
|
|
@@ -206,4 +335,4 @@ declare const appRouter: _trpc_server5.TRPCBuiltRouter<{
|
|
|
206
335
|
}, _trpc_server5.TRPCDecorateCreateRouterOptions<{}>>;
|
|
207
336
|
type AppRouter = typeof appRouter;
|
|
208
337
|
//#endregion
|
|
209
|
-
export { type AnyProcedure, type AppRouter, type CrudOperation, type CrudRouterConfig, type DeclarativeConfig, type FactoryConfig, type ProceduresConfig, type SoftDeleteConfig, type SoftDeleteOption, type WriteOperation, appRouter, createCrudRouter, publicProcedure, router };
|
|
338
|
+
export { type AnyProcedure, type AppRouter, type CrudHooks, type CrudOperation, type CrudRouterConfig, type DeclarativeConfig, type FactoryConfig, type ListInput, type ListResult, type ProceduresConfig, type SoftDeleteConfig, type SoftDeleteOption, type WriteOperation, appRouter, createCrudRouter, publicProcedure, router };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SQL } from "drizzle-orm";
|
|
3
|
-
import * as
|
|
3
|
+
import * as _trpc_server2 from "@trpc/server";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
6
6
|
import * as node_modules__trpc_server_dist_unstable_core_do_not_import_d_CjQPvBRI_mjs0 from "node_modules/@trpc/server/dist/unstable-core-do-not-import.d-CjQPvBRI.mjs";
|
|
@@ -14,13 +14,13 @@ import { PgTable } from "drizzle-orm/pg-core";
|
|
|
14
14
|
interface Context {
|
|
15
15
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
16
16
|
}
|
|
17
|
-
declare const router:
|
|
17
|
+
declare const router: _trpc_server2.TRPCRouterBuilder<{
|
|
18
18
|
ctx: Context;
|
|
19
19
|
meta: object;
|
|
20
|
-
errorShape:
|
|
20
|
+
errorShape: _trpc_server2.TRPCDefaultErrorShape;
|
|
21
21
|
transformer: true;
|
|
22
22
|
}>;
|
|
23
|
-
declare const publicProcedure:
|
|
23
|
+
declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/types/config.d.ts
|
|
26
26
|
type CrudOperation = "list" | "get" | "create" | "update" | "delete" | "deleteMany" | "updateMany";
|
|
@@ -43,7 +43,121 @@ interface SoftDeleteConfig {
|
|
|
43
43
|
* - SoftDeleteConfig: 完整配置
|
|
44
44
|
*/
|
|
45
45
|
type SoftDeleteOption = string | boolean | SoftDeleteConfig;
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 列表查询输入类型
|
|
48
|
+
*/
|
|
49
|
+
interface ListInput {
|
|
50
|
+
page: number;
|
|
51
|
+
perPage: number;
|
|
52
|
+
sort?: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
desc: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
filters?: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
value: string | string[];
|
|
59
|
+
variant: string;
|
|
60
|
+
operator: string;
|
|
61
|
+
filterId: string;
|
|
62
|
+
}>;
|
|
63
|
+
joinOperator: "and" | "or";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 列表查询结果类型
|
|
67
|
+
*/
|
|
68
|
+
interface ListResult<TSelect> {
|
|
69
|
+
data: TSelect[];
|
|
70
|
+
total: number;
|
|
71
|
+
page: number;
|
|
72
|
+
perPage: number;
|
|
73
|
+
pageCount: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* CRUD 生命周期钩子
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* hooks: {
|
|
80
|
+
* beforeCreate: async (ctx, data) => {
|
|
81
|
+
* // 修改数据
|
|
82
|
+
* return { ...data, slug: slugify(data.title) };
|
|
83
|
+
* },
|
|
84
|
+
* afterCreate: async (ctx, created) => {
|
|
85
|
+
* // 发送通知
|
|
86
|
+
* await sendNotification(created);
|
|
87
|
+
* },
|
|
88
|
+
* }
|
|
89
|
+
*/
|
|
90
|
+
interface CrudHooks<TContext = unknown> {
|
|
91
|
+
/**
|
|
92
|
+
* 列表查询前
|
|
93
|
+
* 可修改查询参数
|
|
94
|
+
*/
|
|
95
|
+
beforeList?: (ctx: TContext, input: ListInput) => ListInput | void | Promise<ListInput | void>;
|
|
96
|
+
/**
|
|
97
|
+
* 列表查询后
|
|
98
|
+
* 可修改返回结果
|
|
99
|
+
*/
|
|
100
|
+
afterList?: <TSelect>(ctx: TContext, result: ListResult<TSelect>) => ListResult<TSelect> | void | Promise<ListResult<TSelect> | void>;
|
|
101
|
+
/**
|
|
102
|
+
* 单条查询前
|
|
103
|
+
*/
|
|
104
|
+
beforeGet?: (ctx: TContext, id: string) => void | Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* 单条查询后
|
|
107
|
+
* 可修改返回结果
|
|
108
|
+
*/
|
|
109
|
+
afterGet?: <TSelect>(ctx: TContext, result: TSelect | null) => TSelect | null | void | Promise<TSelect | null | void>;
|
|
110
|
+
/**
|
|
111
|
+
* 创建前
|
|
112
|
+
* 可修改输入数据,返回新数据或抛出错误阻止创建
|
|
113
|
+
*/
|
|
114
|
+
beforeCreate?: <TInsert>(ctx: TContext, data: TInsert) => TInsert | void | Promise<TInsert | void>;
|
|
115
|
+
/**
|
|
116
|
+
* 创建后
|
|
117
|
+
* 用于副作用(日志、通知、同步等)
|
|
118
|
+
*/
|
|
119
|
+
afterCreate?: <TSelect>(ctx: TContext, created: TSelect) => void | Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* 更新前
|
|
122
|
+
* 可修改输入数据,existing 为更新前的记录
|
|
123
|
+
*/
|
|
124
|
+
beforeUpdate?: <TUpdate, TSelect>(ctx: TContext, id: string, data: TUpdate, existing: TSelect) => TUpdate | void | Promise<TUpdate | void>;
|
|
125
|
+
/**
|
|
126
|
+
* 更新后
|
|
127
|
+
*/
|
|
128
|
+
afterUpdate?: <TSelect>(ctx: TContext, updated: TSelect) => void | Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* 删除前
|
|
131
|
+
* existing 为删除前的记录,可抛出错误阻止删除
|
|
132
|
+
*/
|
|
133
|
+
beforeDelete?: <TSelect>(ctx: TContext, id: string, existing: TSelect) => void | Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* 删除后
|
|
136
|
+
*/
|
|
137
|
+
afterDelete?: <TSelect>(ctx: TContext, deleted: TSelect) => void | Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* 批量删除前
|
|
140
|
+
*/
|
|
141
|
+
beforeDeleteMany?: (ctx: TContext, ids: string[]) => void | Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* 批量删除后
|
|
144
|
+
*/
|
|
145
|
+
afterDeleteMany?: (ctx: TContext, result: {
|
|
146
|
+
deleted: number;
|
|
147
|
+
}) => void | Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* 批量更新前
|
|
150
|
+
* 可修改输入数据
|
|
151
|
+
*/
|
|
152
|
+
beforeUpdateMany?: <TUpdate>(ctx: TContext, ids: string[], data: TUpdate) => TUpdate | void | Promise<TUpdate | void>;
|
|
153
|
+
/**
|
|
154
|
+
* 批量更新后
|
|
155
|
+
*/
|
|
156
|
+
afterUpdateMany?: (ctx: TContext, result: {
|
|
157
|
+
updated: number;
|
|
158
|
+
}) => void | Promise<void>;
|
|
159
|
+
}
|
|
160
|
+
interface CrudRouterConfigBase<TContext = unknown> {
|
|
47
161
|
/** Drizzle 表定义 */
|
|
48
162
|
table: PgTable;
|
|
49
163
|
/** 查询返回 Schema */
|
|
@@ -78,8 +192,23 @@ interface CrudRouterConfigBase {
|
|
|
78
192
|
* softDelete: { column: 'isDeleted', value: () => true }
|
|
79
193
|
*/
|
|
80
194
|
softDelete?: SoftDeleteOption;
|
|
195
|
+
/**
|
|
196
|
+
* 生命周期钩子
|
|
197
|
+
* 在 CRUD 操作前后注入自定义逻辑
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* hooks: {
|
|
201
|
+
* beforeCreate: async (ctx, data) => {
|
|
202
|
+
* return { ...data, slug: slugify(data.title) };
|
|
203
|
+
* },
|
|
204
|
+
* afterCreate: async (ctx, created) => {
|
|
205
|
+
* await sendNotification(created);
|
|
206
|
+
* },
|
|
207
|
+
* }
|
|
208
|
+
*/
|
|
209
|
+
hooks?: CrudHooks<TContext>;
|
|
81
210
|
}
|
|
82
|
-
interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
211
|
+
interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
83
212
|
mode?: "declarative";
|
|
84
213
|
/**
|
|
85
214
|
* 基础 procedure(认证)
|
|
@@ -119,7 +248,7 @@ interface DeclarativeConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
|
119
248
|
*/
|
|
120
249
|
authorize?: (ctx: TContext, resource: Record<string, unknown>, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
121
250
|
}
|
|
122
|
-
interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
251
|
+
interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
123
252
|
mode: "factory";
|
|
124
253
|
/**
|
|
125
254
|
* Procedure 工厂函数
|
|
@@ -138,7 +267,7 @@ interface FactoryConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
|
138
267
|
*/
|
|
139
268
|
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
140
269
|
}
|
|
141
|
-
interface ProceduresConfig<TContext = unknown> extends CrudRouterConfigBase {
|
|
270
|
+
interface ProceduresConfig<TContext = unknown> extends CrudRouterConfigBase<TContext> {
|
|
142
271
|
mode: "procedures";
|
|
143
272
|
/**
|
|
144
273
|
* 按操作指定不同的 procedure
|
|
@@ -168,9 +297,9 @@ type CrudRouterConfig<TContext = unknown> = DeclarativeConfig<TContext> | Factor
|
|
|
168
297
|
declare function createCrudRouter<TContext = unknown>(config: CrudRouterConfig<TContext>): node_modules__trpc_server_dist_unstable_core_do_not_import_d_CjQPvBRI_mjs0.Router<{
|
|
169
298
|
ctx: Context;
|
|
170
299
|
meta: object;
|
|
171
|
-
errorShape:
|
|
300
|
+
errorShape: _trpc_server2.TRPCDefaultErrorShape;
|
|
172
301
|
transformer: true;
|
|
173
|
-
},
|
|
302
|
+
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{
|
|
174
303
|
list: any;
|
|
175
304
|
getById: any;
|
|
176
305
|
create: any;
|
|
@@ -178,7 +307,7 @@ declare function createCrudRouter<TContext = unknown>(config: CrudRouterConfig<T
|
|
|
178
307
|
delete: any;
|
|
179
308
|
deleteMany: any;
|
|
180
309
|
updateMany: any;
|
|
181
|
-
}>> &
|
|
310
|
+
}>> & _trpc_server2.TRPCDecorateCreateRouterOptions<{
|
|
182
311
|
list: any;
|
|
183
312
|
getById: any;
|
|
184
313
|
create: any;
|
|
@@ -199,12 +328,12 @@ declare function createCrudRouter<TContext = unknown>(config: CrudRouterConfig<T
|
|
|
199
328
|
};
|
|
200
329
|
//#endregion
|
|
201
330
|
//#region src/routers/index.d.ts
|
|
202
|
-
declare const appRouter:
|
|
331
|
+
declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
203
332
|
ctx: Context;
|
|
204
333
|
meta: object;
|
|
205
|
-
errorShape:
|
|
334
|
+
errorShape: _trpc_server2.TRPCDefaultErrorShape;
|
|
206
335
|
transformer: true;
|
|
207
|
-
},
|
|
336
|
+
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
208
337
|
type AppRouter = typeof appRouter;
|
|
209
338
|
//#endregion
|
|
210
|
-
export { type AnyProcedure, type AppRouter, type CrudOperation, type CrudRouterConfig, type DeclarativeConfig, type FactoryConfig, type ProceduresConfig, type SoftDeleteConfig, type SoftDeleteOption, type WriteOperation, appRouter, createCrudRouter, publicProcedure, router };
|
|
339
|
+
export { type AnyProcedure, type AppRouter, type CrudHooks, type CrudOperation, type CrudRouterConfig, type DeclarativeConfig, type FactoryConfig, type ListInput, type ListResult, type ProceduresConfig, type SoftDeleteConfig, type SoftDeleteOption, type WriteOperation, appRouter, createCrudRouter, publicProcedure, router };
|
package/dist/index.js
CHANGED
|
@@ -447,9 +447,10 @@ function validateColumn(columnId, allowedColumns) {
|
|
|
447
447
|
return allowedColumns.includes(columnId);
|
|
448
448
|
}
|
|
449
449
|
function createCrudRouter(config) {
|
|
450
|
-
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption } = config;
|
|
450
|
+
const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, hooks = {} } = config;
|
|
451
451
|
const resolved = resolveConfig(config);
|
|
452
452
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
453
|
+
const h = hooks;
|
|
453
454
|
const getIdColumn = () => table[idField];
|
|
454
455
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
455
456
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
@@ -477,49 +478,71 @@ function createCrudRouter(config) {
|
|
|
477
478
|
const procedures = {
|
|
478
479
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
479
480
|
await withGuard(ctx, "list");
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
let processedInput = input;
|
|
482
|
+
if (h.beforeList) {
|
|
483
|
+
const result = await h.beforeList(ctx, processedInput);
|
|
484
|
+
if (result) processedInput = result;
|
|
485
|
+
}
|
|
486
|
+
const offset = (processedInput.page - 1) * processedInput.perPage;
|
|
487
|
+
const validatedFilters = processedInput.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
|
|
482
488
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
483
489
|
table,
|
|
484
490
|
filters: validatedFilters,
|
|
485
|
-
joinOperator:
|
|
491
|
+
joinOperator: processedInput.joinOperator
|
|
486
492
|
}) : void 0);
|
|
487
493
|
let query = ctx.db.select().from(table).$dynamic();
|
|
488
494
|
if (where) query = query.where(where);
|
|
489
|
-
if (
|
|
490
|
-
const sortField =
|
|
495
|
+
if (processedInput.sort?.length) {
|
|
496
|
+
const sortField = processedInput.sort[0];
|
|
491
497
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
492
498
|
const column = table[sortField.id];
|
|
493
499
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
494
500
|
}
|
|
495
501
|
}
|
|
496
|
-
const data = await query.limit(
|
|
502
|
+
const data = await query.limit(processedInput.perPage).offset(offset);
|
|
497
503
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
498
504
|
if (where) countQuery = countQuery.where(where);
|
|
499
505
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
500
|
-
|
|
506
|
+
let listResult = {
|
|
501
507
|
data,
|
|
502
508
|
total: count,
|
|
503
|
-
page:
|
|
504
|
-
perPage:
|
|
505
|
-
pageCount: Math.ceil(count /
|
|
509
|
+
page: processedInput.page,
|
|
510
|
+
perPage: processedInput.perPage,
|
|
511
|
+
pageCount: Math.ceil(count / processedInput.perPage)
|
|
506
512
|
};
|
|
513
|
+
if (h.afterList) {
|
|
514
|
+
const result = await h.afterList(ctx, listResult);
|
|
515
|
+
if (result) listResult = result;
|
|
516
|
+
}
|
|
517
|
+
return listResult;
|
|
507
518
|
}),
|
|
508
519
|
getById: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
|
|
509
520
|
await withGuard(ctx, "get");
|
|
521
|
+
if (h.beforeGet) await h.beforeGet(ctx, input);
|
|
510
522
|
const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
|
|
511
523
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
512
524
|
await withAuthorize(ctx, item, "get");
|
|
513
|
-
|
|
525
|
+
let result = item ?? null;
|
|
526
|
+
if (h.afterGet) {
|
|
527
|
+
const hookResult = await h.afterGet(ctx, result);
|
|
528
|
+
if (hookResult !== void 0) result = hookResult;
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
514
531
|
}),
|
|
515
532
|
create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
516
533
|
await withGuard(ctx, "create");
|
|
534
|
+
let processedData = input;
|
|
535
|
+
if (h.beforeCreate) {
|
|
536
|
+
const result = await h.beforeCreate(ctx, processedData);
|
|
537
|
+
if (result) processedData = result;
|
|
538
|
+
}
|
|
517
539
|
const injectData = resolved.getInject(ctx, "create");
|
|
518
540
|
const data = {
|
|
519
|
-
...
|
|
541
|
+
...processedData,
|
|
520
542
|
...injectData
|
|
521
543
|
};
|
|
522
544
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
545
|
+
if (h.afterCreate) await h.afterCreate(ctx, created);
|
|
523
546
|
return created;
|
|
524
547
|
}),
|
|
525
548
|
update: resolved.procedureFactory("update").input(z.object({
|
|
@@ -531,12 +554,18 @@ function createCrudRouter(config) {
|
|
|
531
554
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
532
555
|
await withAuthorize(ctx, existing, "update");
|
|
533
556
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
557
|
+
let processedData = input.data;
|
|
558
|
+
if (h.beforeUpdate) {
|
|
559
|
+
const result = await h.beforeUpdate(ctx, input.id, processedData, existing);
|
|
560
|
+
if (result) processedData = result;
|
|
561
|
+
}
|
|
534
562
|
const injectData = resolved.getInject(ctx, "update");
|
|
535
563
|
const data = {
|
|
536
|
-
...
|
|
564
|
+
...processedData,
|
|
537
565
|
...injectData
|
|
538
566
|
};
|
|
539
567
|
const [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
568
|
+
if (h.afterUpdate) await h.afterUpdate(ctx, updated);
|
|
540
569
|
return updated;
|
|
541
570
|
}),
|
|
542
571
|
delete: resolved.procedureFactory("delete").input(z.string()).mutation(async ({ ctx, input }) => {
|
|
@@ -545,31 +574,42 @@ function createCrudRouter(config) {
|
|
|
545
574
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
546
575
|
await withAuthorize(ctx, existing, "delete");
|
|
547
576
|
if (!existing) throw new Error("Resource not found or access denied");
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
577
|
+
if (h.beforeDelete) await h.beforeDelete(ctx, input, existing);
|
|
578
|
+
let deleted;
|
|
579
|
+
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
580
|
+
else [deleted] = await ctx.db.delete(table).where(where).returning();
|
|
581
|
+
if (h.afterDelete) await h.afterDelete(ctx, deleted);
|
|
553
582
|
return deleted;
|
|
554
583
|
}),
|
|
555
584
|
deleteMany: resolved.procedureFactory("deleteMany").input(z.array(z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
|
|
556
585
|
await withGuard(ctx, "deleteMany");
|
|
586
|
+
if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
|
|
557
587
|
const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
|
|
558
|
-
|
|
559
|
-
|
|
588
|
+
let result;
|
|
589
|
+
if (softDelete) result = { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
590
|
+
else result = { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
591
|
+
if (h.afterDeleteMany) await h.afterDeleteMany(ctx, result);
|
|
592
|
+
return result;
|
|
560
593
|
}),
|
|
561
594
|
updateMany: resolved.procedureFactory("updateMany").input(z.object({
|
|
562
595
|
ids: z.array(z.string()).max(maxBatchSize),
|
|
563
596
|
data: updateSchema
|
|
564
597
|
})).mutation(async ({ ctx, input }) => {
|
|
565
598
|
await withGuard(ctx, "updateMany");
|
|
599
|
+
let processedData = input.data;
|
|
600
|
+
if (h.beforeUpdateMany) {
|
|
601
|
+
const result$1 = await h.beforeUpdateMany(ctx, input.ids, processedData);
|
|
602
|
+
if (result$1) processedData = result$1;
|
|
603
|
+
}
|
|
566
604
|
const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
|
|
567
605
|
const injectData = resolved.getInject(ctx, "update");
|
|
568
606
|
const data = {
|
|
569
|
-
...
|
|
607
|
+
...processedData,
|
|
570
608
|
...injectData
|
|
571
609
|
};
|
|
572
|
-
|
|
610
|
+
const result = { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
611
|
+
if (h.afterUpdateMany) await h.afterUpdateMany(ctx, result);
|
|
612
|
+
return result;
|
|
573
613
|
})
|
|
574
614
|
};
|
|
575
615
|
const crudRouter = router(procedures);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
48
|
"zod": "^4.1.13",
|
|
49
49
|
"@internal/eslint-config": "0.3.0",
|
|
50
|
-
"@internal/prettier-config": "0.0.1",
|
|
51
50
|
"@internal/tsconfig": "0.1.0",
|
|
51
|
+
"@internal/prettier-config": "0.0.1",
|
|
52
52
|
"@internal/tsdown-config": "0.1.0",
|
|
53
53
|
"@internal/vitest-config": "0.1.0"
|
|
54
54
|
},
|