@wordrhyme/auto-crud-server 0.3.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,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, middleware = {} } = config;
479
479
  const resolved = resolveConfig(config);
480
480
  const softDelete = resolveSoftDelete(softDeleteOption);
481
+ const m = middleware;
481
482
  const getIdColumn = () => table[idField];
482
483
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
483
484
  const buildWhere = (ctx, operation, additionalCondition) => {
@@ -505,50 +506,75 @@ 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));
510
- const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
511
- table,
512
- filters: validatedFilters,
513
- joinOperator: input.joinOperator
514
- }) : void 0);
515
- let query = ctx.db.select().from(table).$dynamic();
516
- if (where) query = query.where(where);
517
- if (input.sort?.length) {
518
- const sortField = input.sort[0];
519
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
520
- const column = table[sortField.id];
521
- 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
+ }
522
525
  }
523
- }
524
- const data = await query.limit(input.perPage).offset(offset);
525
- let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
526
- if (where) countQuery = countQuery.where(where);
527
- const count = (await countQuery)[0]?.count ?? 0;
528
- return {
529
- data,
530
- total: count,
531
- page: input.page,
532
- perPage: input.perPage,
533
- pageCount: Math.ceil(count / input.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
+ };
534
537
  };
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);
535
545
  }),
536
546
  getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
537
547
  await withGuard(ctx, "get");
538
- const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
539
- const [item] = await ctx.db.select().from(table).where(where);
540
- await withAuthorize(ctx, item, "get");
541
- return item ?? null;
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();
542
560
  }),
543
561
  create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
544
562
  await withGuard(ctx, "create");
545
- const injectData = resolved.getInject(ctx, "create");
546
- const data = {
547
- ...input,
548
- ...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;
549
571
  };
550
- const [created] = await ctx.db.insert(table).values(data).returning();
551
- 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);
552
578
  }),
553
579
  update: resolved.procedureFactory("update").input(zod.z.object({
554
580
  id: zod.z.string(),
@@ -559,13 +585,23 @@ function createCrudRouter(config) {
559
585
  const [existing] = await ctx.db.select().from(table).where(where);
560
586
  await withAuthorize(ctx, existing, "update");
561
587
  if (!existing) throw new Error("Resource not found or access denied");
562
- const injectData = resolved.getInject(ctx, "update");
563
- const data = {
564
- ...input.data,
565
- ...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;
566
596
  };
567
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
568
- 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);
569
605
  }),
570
606
  delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
571
607
  await withGuard(ctx, "delete");
@@ -573,18 +609,33 @@ function createCrudRouter(config) {
573
609
  const [existing] = await ctx.db.select().from(table).where(where);
574
610
  await withAuthorize(ctx, existing, "delete");
575
611
  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();
581
- 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();
582
625
  }),
583
626
  deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
584
627
  await withGuard(ctx, "deleteMany");
585
628
  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 };
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();
588
639
  }),
589
640
  updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
590
641
  ids: zod.z.array(zod.z.string()).max(maxBatchSize),
@@ -592,24 +643,158 @@ function createCrudRouter(config) {
592
643
  })).mutation(async ({ ctx, input }) => {
593
644
  await withGuard(ctx, "updateMany");
594
645
  const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
595
- const injectData = resolved.getInject(ctx, "update");
596
- const data = {
597
- ...input.data,
598
- ...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 };
599
653
  };
600
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
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);
601
661
  })
602
662
  };
603
663
  const crudRouter = router(procedures);
604
664
  return Object.assign(crudRouter, { procedures });
605
665
  }
606
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
+
607
782
  //#endregion
608
783
  //#region src/routers/index.ts
609
784
  const appRouter = router({});
610
785
 
611
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;
612
793
  exports.appRouter = appRouter;
794
+ exports.beforeCreate = beforeCreate;
795
+ exports.beforeList = beforeList;
796
+ exports.beforeMiddleware = beforeMiddleware;
797
+ exports.composeMiddleware = composeMiddleware;
613
798
  exports.createCrudRouter = createCrudRouter;
614
799
  exports.publicProcedure = publicProcedure;
615
800
  exports.router = router;