@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 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
- const offset = (input.page - 1) * input.perPage;
509
- const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
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: input.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 (input.sort?.length) {
518
- const sortField = input.sort[0];
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(input.perPage).offset(offset);
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
- return {
534
+ let listResult = {
529
535
  data,
530
536
  total: count,
531
- page: input.page,
532
- perPage: input.perPage,
533
- pageCount: Math.ceil(count / input.perPage)
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
- return item ?? null;
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
- ...input,
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
- ...input.data,
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 (softDelete) {
577
- const [deleted$1] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
578
- return deleted$1;
579
- }
580
- const [deleted] = await ctx.db.delete(table).where(where).returning();
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
- if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
587
- return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
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
- ...input.data,
635
+ ...processedData,
598
636
  ...injectData
599
637
  };
600
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
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
- interface CrudRouterConfigBase {
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 _trpc_server5 from "@trpc/server";
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: _trpc_server5.TRPCRouterBuilder<{
17
+ declare const router: _trpc_server2.TRPCRouterBuilder<{
18
18
  ctx: Context;
19
19
  meta: object;
20
- errorShape: _trpc_server5.TRPCDefaultErrorShape;
20
+ errorShape: _trpc_server2.TRPCDefaultErrorShape;
21
21
  transformer: true;
22
22
  }>;
23
- declare const publicProcedure: _trpc_server5.TRPCProcedureBuilder<Context, object, object, _trpc_server5.TRPCUnsetMarker, _trpc_server5.TRPCUnsetMarker, _trpc_server5.TRPCUnsetMarker, _trpc_server5.TRPCUnsetMarker, false>;
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
- interface CrudRouterConfigBase {
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: _trpc_server5.TRPCDefaultErrorShape;
300
+ errorShape: _trpc_server2.TRPCDefaultErrorShape;
172
301
  transformer: true;
173
- }, _trpc_server5.TRPCDecorateCreateRouterOptions<{
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
- }>> & _trpc_server5.TRPCDecorateCreateRouterOptions<{
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: _trpc_server5.TRPCBuiltRouter<{
331
+ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
203
332
  ctx: Context;
204
333
  meta: object;
205
- errorShape: _trpc_server5.TRPCDefaultErrorShape;
334
+ errorShape: _trpc_server2.TRPCDefaultErrorShape;
206
335
  transformer: true;
207
- }, _trpc_server5.TRPCDecorateCreateRouterOptions<{}>>;
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
- const offset = (input.page - 1) * input.perPage;
481
- const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
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: input.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 (input.sort?.length) {
490
- const sortField = input.sort[0];
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(input.perPage).offset(offset);
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
- return {
506
+ let listResult = {
501
507
  data,
502
508
  total: count,
503
- page: input.page,
504
- perPage: input.perPage,
505
- pageCount: Math.ceil(count / input.perPage)
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
- return item ?? null;
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
- ...input,
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
- ...input.data,
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 (softDelete) {
549
- const [deleted$1] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
550
- return deleted$1;
551
- }
552
- const [deleted] = await ctx.db.delete(table).where(where).returning();
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
- if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
559
- return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
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
- ...input.data,
607
+ ...processedData,
570
608
  ...injectData
571
609
  };
572
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
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.3.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
  },