@wordrhyme/auto-crud-server 0.4.0 → 0.6.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/README.md CHANGED
@@ -608,6 +608,110 @@ export const tasksRouter = router({
608
608
 
609
609
  ---
610
610
 
611
+ ## 🔄 生命周期中间件
612
+
613
+ 使用 `middleware` 配置在 CRUD 操作前后注入自定义逻辑,类似 tRPC middleware 模式。
614
+
615
+ ### 完整控制模式
616
+
617
+ ```typescript
618
+ import { createCrudRouter } from "@wordrhyme/auto-crud-server";
619
+
620
+ const tasksRouter = createCrudRouter({
621
+ table: tasks,
622
+ insertSchema: insertTaskSchema,
623
+ updateSchema: updateTaskSchema,
624
+
625
+ // 中间件:完全控制操作流程
626
+ middleware: {
627
+ // 创建:修改输入 + 执行副作用
628
+ create: async ({ ctx, input, next }) => {
629
+ // 1. 修改输入数据
630
+ const data = { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
631
+
632
+ // 2. 执行核心操作
633
+ const result = await next(data);
634
+
635
+ // 3. 执行副作用
636
+ await sendNotification(result);
637
+ await logAudit(ctx.user, "create", result);
638
+
639
+ // 4. 返回结果(可修改)
640
+ return result;
641
+ },
642
+
643
+ // 更新:可访问原记录
644
+ update: async ({ ctx, id, data, existing, next }) => {
645
+ // 检查权限
646
+ if (existing.ownerId !== ctx.user.id) {
647
+ throw new Error("Forbidden");
648
+ }
649
+ return next(data);
650
+ },
651
+
652
+ // 删除:条件拦截
653
+ delete: async ({ ctx, id, existing, next }) => {
654
+ if (existing.status === "locked") {
655
+ throw new Error("Cannot delete locked resource");
656
+ }
657
+ return next();
658
+ },
659
+ },
660
+ });
661
+ ```
662
+
663
+ ### 使用便捷工具函数
664
+
665
+ 对于简单场景,使用工具函数简化代码:
666
+
667
+ ```typescript
668
+ import {
669
+ createCrudRouter,
670
+ afterMiddleware,
671
+ beforeMiddleware,
672
+ afterCreate,
673
+ beforeCreate,
674
+ } from "@wordrhyme/auto-crud-server";
675
+
676
+ const tasksRouter = createCrudRouter({
677
+ table: tasks,
678
+ insertSchema: insertTaskSchema,
679
+ updateSchema: updateTaskSchema,
680
+
681
+ middleware: {
682
+ // 简单副作用:只在操作后执行
683
+ create: afterMiddleware(async (ctx, result) => {
684
+ await sendEmail(result);
685
+ await logAudit(ctx.user, "create", result);
686
+ }),
687
+
688
+ // 修改输入:只在操作前执行
689
+ update: beforeMiddleware(async (ctx, data) => {
690
+ return { ...data, updatedAt: new Date(), updatedBy: ctx.user.id };
691
+ }),
692
+
693
+ // 类型安全的便捷函数
694
+ delete: afterDelete(async (ctx, deleted) => {
695
+ await cleanupRelatedData(deleted.id);
696
+ }),
697
+ },
698
+ });
699
+ ```
700
+
701
+ ### 可用的工具函数
702
+
703
+ | 函数 | 用途 | 示例 |
704
+ |------|------|------|
705
+ | `afterMiddleware(fn)` | 操作后执行副作用 | 日志、通知、审计 |
706
+ | `afterMiddlewareTransform(fn)` | 操作后修改返回值 | 添加计算字段 |
707
+ | `beforeMiddleware(fn)` | 操作前修改输入 | 注入用户ID、生成slug |
708
+ | `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
709
+ | `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
710
+ | `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
711
+ | `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
712
+
713
+ ---
714
+
611
715
  ## 🔌 与其他库集成
612
716
 
613
717
  ### 与 Drizzle ORM 集成
@@ -698,7 +802,26 @@ export const filterVariants = {
698
802
  ```typescript
699
803
  // 主要导出
700
804
  export { createCrudRouter } from "./routers/_factory";
701
- export type { CrudRouterConfig } from "./routers/_factory";
805
+ export type {
806
+ CrudRouterConfig,
807
+ CrudMiddleware,
808
+ ListInput,
809
+ ListResult,
810
+ } from "./types/config";
811
+
812
+ // Middleware 工具函数
813
+ export {
814
+ afterMiddleware,
815
+ afterMiddlewareTransform,
816
+ beforeMiddleware,
817
+ composeMiddleware,
818
+ afterList,
819
+ beforeList,
820
+ afterCreate,
821
+ beforeCreate,
822
+ afterUpdate,
823
+ afterDelete,
824
+ } from "./lib/middleware-helpers";
702
825
 
703
826
  // tRPC 工具
704
827
  export { router, publicProcedure } from "./trpc";
package/dist/index.cjs CHANGED
@@ -475,10 +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, hooks = {} } = config;
478
+ const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
479
479
  const resolved = resolveConfig(config);
480
480
  const softDelete = resolveSoftDelete(softDeleteOption);
481
- const h = hooks;
481
+ const m = middleware;
482
482
  const getIdColumn = () => table[idField];
483
483
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
484
484
  const buildWhere = (ctx, operation, additionalCondition) => {
@@ -506,72 +506,75 @@ function createCrudRouter(config) {
506
506
  const procedures = {
507
507
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
508
508
  await withGuard(ctx, "list");
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));
516
- const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
517
- table,
518
- filters: validatedFilters,
519
- joinOperator: processedInput.joinOperator
520
- }) : void 0);
521
- let query = ctx.db.select().from(table).$dynamic();
522
- if (where) query = query.where(where);
523
- if (processedInput.sort?.length) {
524
- const sortField = processedInput.sort[0];
525
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
526
- const column = table[sortField.id];
527
- if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
509
+ const doList = async (listInput$1) => {
510
+ const offset = (listInput$1.page - 1) * listInput$1.perPage;
511
+ const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
512
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
513
+ table,
514
+ filters: validatedFilters,
515
+ joinOperator: listInput$1.joinOperator
516
+ }) : void 0);
517
+ let query = ctx.db.select().from(table).$dynamic();
518
+ if (where) query = query.where(where);
519
+ if (listInput$1.sort?.length) {
520
+ const sortField = listInput$1.sort[0];
521
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
522
+ const column = table[sortField.id];
523
+ if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
524
+ }
528
525
  }
529
- }
530
- const data = await query.limit(processedInput.perPage).offset(offset);
531
- let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
532
- if (where) countQuery = countQuery.where(where);
533
- const count = (await countQuery)[0]?.count ?? 0;
534
- let listResult = {
535
- data,
536
- total: count,
537
- page: processedInput.page,
538
- perPage: processedInput.perPage,
539
- pageCount: Math.ceil(count / processedInput.perPage)
526
+ const data = await query.limit(listInput$1.perPage).offset(offset);
527
+ let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
528
+ if (where) countQuery = countQuery.where(where);
529
+ const count = (await countQuery)[0]?.count ?? 0;
530
+ return {
531
+ data,
532
+ total: count,
533
+ page: listInput$1.page,
534
+ perPage: listInput$1.perPage,
535
+ pageCount: Math.ceil(count / listInput$1.perPage)
536
+ };
540
537
  };
541
- if (h.afterList) {
542
- const result = await h.afterList(ctx, listResult);
543
- if (result) listResult = result;
544
- }
545
- return listResult;
538
+ const listInput = input;
539
+ if (m.list) return m.list({
540
+ ctx,
541
+ input: listInput,
542
+ next: async (modifiedInput) => doList(modifiedInput ?? listInput)
543
+ });
544
+ return doList(listInput);
546
545
  }),
547
546
  getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
548
547
  await withGuard(ctx, "get");
549
- if (h.beforeGet) await h.beforeGet(ctx, input);
550
- const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
551
- const [item] = await ctx.db.select().from(table).where(where);
552
- await withAuthorize(ctx, item, "get");
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;
548
+ const doGet = async () => {
549
+ const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
550
+ const [item] = await ctx.db.select().from(table).where(where);
551
+ await withAuthorize(ctx, item, "get");
552
+ return item ?? null;
553
+ };
554
+ if (m.get) return m.get({
555
+ ctx,
556
+ id: input,
557
+ next: doGet
558
+ });
559
+ return doGet();
559
560
  }),
560
561
  create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
561
562
  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
- }
567
- const injectData = resolved.getInject(ctx, "create");
568
- const data = {
569
- ...processedData,
570
- ...injectData
563
+ const doCreate = async (inputData) => {
564
+ const injectData = resolved.getInject(ctx, "create");
565
+ const data = {
566
+ ...inputData,
567
+ ...injectData
568
+ };
569
+ const [created] = await ctx.db.insert(table).values(data).returning();
570
+ return created;
571
571
  };
572
- const [created] = await ctx.db.insert(table).values(data).returning();
573
- if (h.afterCreate) await h.afterCreate(ctx, created);
574
- return created;
572
+ if (m.create) return m.create({
573
+ ctx,
574
+ input,
575
+ next: async (modifiedInput) => doCreate(modifiedInput ?? input)
576
+ });
577
+ return doCreate(input);
575
578
  }),
576
579
  update: resolved.procedureFactory("update").input(zod.z.object({
577
580
  id: zod.z.string(),
@@ -582,19 +585,23 @@ function createCrudRouter(config) {
582
585
  const [existing] = await ctx.db.select().from(table).where(where);
583
586
  await withAuthorize(ctx, existing, "update");
584
587
  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
- }
590
- const injectData = resolved.getInject(ctx, "update");
591
- const data = {
592
- ...processedData,
593
- ...injectData
588
+ const doUpdate = async (updateData) => {
589
+ const injectData = resolved.getInject(ctx, "update");
590
+ const data = {
591
+ ...updateData,
592
+ ...injectData
593
+ };
594
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
595
+ return updated;
594
596
  };
595
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
596
- if (h.afterUpdate) await h.afterUpdate(ctx, updated);
597
- return updated;
597
+ if (m.update) return m.update({
598
+ ctx,
599
+ id: input.id,
600
+ data: input.data,
601
+ existing,
602
+ next: async (modifiedData) => doUpdate(modifiedData ?? input.data)
603
+ });
604
+ return doUpdate(input.data);
598
605
  }),
599
606
  delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
600
607
  await withGuard(ctx, "delete");
@@ -602,54 +609,192 @@ function createCrudRouter(config) {
602
609
  const [existing] = await ctx.db.select().from(table).where(where);
603
610
  await withAuthorize(ctx, existing, "delete");
604
611
  if (!existing) throw new Error("Resource not found or access denied");
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);
610
- return deleted;
612
+ const doDelete = async () => {
613
+ let deleted;
614
+ if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
615
+ else [deleted] = await ctx.db.delete(table).where(where).returning();
616
+ return deleted;
617
+ };
618
+ if (m.delete) return m.delete({
619
+ ctx,
620
+ id: input,
621
+ existing,
622
+ next: doDelete
623
+ });
624
+ return doDelete();
611
625
  }),
612
626
  deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
613
627
  await withGuard(ctx, "deleteMany");
614
- if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
615
628
  const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
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;
629
+ 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 };
632
+ };
633
+ if (m.deleteMany) return m.deleteMany({
634
+ ctx,
635
+ ids: input,
636
+ next: doDeleteMany
637
+ });
638
+ return doDeleteMany();
621
639
  }),
622
640
  updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
623
641
  ids: zod.z.array(zod.z.string()).max(maxBatchSize),
624
642
  data: updateSchema
625
643
  })).mutation(async ({ ctx, input }) => {
626
644
  await withGuard(ctx, "updateMany");
627
- let processedData = input.data;
628
- if (h.beforeUpdateMany) {
629
- const result$1 = await h.beforeUpdateMany(ctx, input.ids, processedData);
630
- if (result$1) processedData = result$1;
631
- }
632
645
  const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
633
- const injectData = resolved.getInject(ctx, "update");
634
- const data = {
635
- ...processedData,
636
- ...injectData
646
+ const doUpdateMany = async (updateData) => {
647
+ const injectData = resolved.getInject(ctx, "update");
648
+ const data = {
649
+ ...updateData,
650
+ ...injectData
651
+ };
652
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
637
653
  };
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;
654
+ if (m.updateMany) return m.updateMany({
655
+ ctx,
656
+ ids: input.ids,
657
+ data: input.data,
658
+ next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
659
+ });
660
+ return doUpdateMany(input.data);
641
661
  })
642
662
  };
643
663
  const crudRouter = router(procedures);
644
664
  return Object.assign(crudRouter, { procedures });
645
665
  }
646
666
 
667
+ //#endregion
668
+ //#region src/lib/middleware-helpers.ts
669
+ /**
670
+ * 创建 after-only 的 middleware(最常见场景)
671
+ * 在操作完成后执行副作用,不修改返回值
672
+ *
673
+ * @example
674
+ * middleware: {
675
+ * create: afterMiddleware(async (ctx, result) => {
676
+ * await sendEmail(result);
677
+ * await logAudit(ctx.user, 'create', result);
678
+ * }),
679
+ * }
680
+ */
681
+ function afterMiddleware(fn) {
682
+ return async ({ ctx, next }) => {
683
+ const result = await next();
684
+ await fn(ctx, result);
685
+ return result;
686
+ };
687
+ }
688
+ /**
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
+ * 创建 before-only 的 middleware
705
+ * 在操作执行前修改输入数据
706
+ *
707
+ * @example
708
+ * middleware: {
709
+ * create: beforeMiddleware(async (ctx, input) => {
710
+ * return { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
711
+ * }),
712
+ * }
713
+ */
714
+ function beforeMiddleware(fn) {
715
+ return async ({ ctx, input, next }) => {
716
+ return next(await fn(ctx, input));
717
+ };
718
+ }
719
+ /**
720
+ * 组合多个 middleware 为一个
721
+ *
722
+ * @example
723
+ * middleware: {
724
+ * create: composeMiddleware(
725
+ * beforeMiddleware((ctx, input) => ({ ...input, slug: slugify(input.title) })),
726
+ * afterMiddleware((ctx, result) => sendEmail(result)),
727
+ * ),
728
+ * }
729
+ */
730
+ function composeMiddleware(...middlewares) {
731
+ return async (params) => {
732
+ let index = 0;
733
+ const executeNext = async (input) => {
734
+ if (index >= middlewares.length) return params.next(input);
735
+ const currentMiddleware = middlewares[index++];
736
+ return currentMiddleware({
737
+ ctx: params.ctx,
738
+ input,
739
+ next: executeNext
740
+ });
741
+ };
742
+ return executeNext(params.input);
743
+ };
744
+ }
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
+
647
782
  //#endregion
648
783
  //#region src/routers/index.ts
649
784
  const appRouter = router({});
650
785
 
651
786
  //#endregion
787
+ exports.afterCreate = afterCreate;
788
+ exports.afterDelete = afterDelete;
789
+ exports.afterList = afterList;
790
+ exports.afterMiddleware = afterMiddleware;
791
+ exports.afterMiddlewareTransform = afterMiddlewareTransform;
792
+ exports.afterUpdate = afterUpdate;
652
793
  exports.appRouter = appRouter;
794
+ exports.beforeCreate = beforeCreate;
795
+ exports.beforeList = beforeList;
796
+ exports.beforeMiddleware = beforeMiddleware;
797
+ exports.composeMiddleware = composeMiddleware;
653
798
  exports.createCrudRouter = createCrudRouter;
654
799
  exports.publicProcedure = publicProcedure;
655
800
  exports.router = router;