@wordrhyme/auto-crud-server 0.4.0 → 0.6.1

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
@@ -136,7 +136,7 @@ export { handler as GET, handler as POST };
136
136
 
137
137
  ## 📖 自动生成的路由
138
138
 
139
- `createCrudRouter` 会自动生成以下 7 个路由:
139
+ `createCrudRouter` 会自动生成以下 8 个路由:
140
140
 
141
141
  ### 1. `list` - 列表查询
142
142
 
@@ -299,6 +299,42 @@ await trpc.tasks.updateMany({
299
299
  });
300
300
  ```
301
301
 
302
+ ### 8. `upsert` - 存在则更新,不存在则创建
303
+
304
+ **输入**:
305
+ ```typescript
306
+ Omit<Task, "createdAt" | "updatedAt"> // 需要包含 id
307
+ ```
308
+
309
+ **输出**:
310
+ ```typescript
311
+ {
312
+ data: Task; // 创建或更新后的记录
313
+ isNew: boolean; // true = 新建, false = 更新
314
+ }
315
+ ```
316
+
317
+ **示例**:
318
+ ```typescript
319
+ // 如果 id="123" 存在则更新,不存在则创建
320
+ const { data, isNew } = await trpc.tasks.upsert({
321
+ id: "123",
322
+ title: "My Task",
323
+ status: "todo",
324
+ });
325
+
326
+ if (isNew) {
327
+ console.log("Created new task");
328
+ } else {
329
+ console.log("Updated existing task");
330
+ }
331
+ ```
332
+
333
+ **适用场景**:
334
+ - 同步外部数据
335
+ - 幂等导入
336
+ - 配置项更新
337
+
302
338
  ---
303
339
 
304
340
  ## 🎯 高级过滤
@@ -608,6 +644,110 @@ export const tasksRouter = router({
608
644
 
609
645
  ---
610
646
 
647
+ ## 🔄 生命周期中间件
648
+
649
+ 使用 `middleware` 配置在 CRUD 操作前后注入自定义逻辑,类似 tRPC middleware 模式。
650
+
651
+ ### 完整控制模式
652
+
653
+ ```typescript
654
+ import { createCrudRouter } from "@wordrhyme/auto-crud-server";
655
+
656
+ const tasksRouter = createCrudRouter({
657
+ table: tasks,
658
+ insertSchema: insertTaskSchema,
659
+ updateSchema: updateTaskSchema,
660
+
661
+ // 中间件:完全控制操作流程
662
+ middleware: {
663
+ // 创建:修改输入 + 执行副作用
664
+ create: async ({ ctx, input, next }) => {
665
+ // 1. 修改输入数据
666
+ const data = { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
667
+
668
+ // 2. 执行核心操作
669
+ const result = await next(data);
670
+
671
+ // 3. 执行副作用
672
+ await sendNotification(result);
673
+ await logAudit(ctx.user, "create", result);
674
+
675
+ // 4. 返回结果(可修改)
676
+ return result;
677
+ },
678
+
679
+ // 更新:可访问原记录
680
+ update: async ({ ctx, id, data, existing, next }) => {
681
+ // 检查权限
682
+ if (existing.ownerId !== ctx.user.id) {
683
+ throw new Error("Forbidden");
684
+ }
685
+ return next(data);
686
+ },
687
+
688
+ // 删除:条件拦截
689
+ delete: async ({ ctx, id, existing, next }) => {
690
+ if (existing.status === "locked") {
691
+ throw new Error("Cannot delete locked resource");
692
+ }
693
+ return next();
694
+ },
695
+ },
696
+ });
697
+ ```
698
+
699
+ ### 使用便捷工具函数
700
+
701
+ 对于简单场景,使用工具函数简化代码:
702
+
703
+ ```typescript
704
+ import {
705
+ createCrudRouter,
706
+ afterMiddleware,
707
+ beforeMiddleware,
708
+ afterCreate,
709
+ beforeCreate,
710
+ } from "@wordrhyme/auto-crud-server";
711
+
712
+ const tasksRouter = createCrudRouter({
713
+ table: tasks,
714
+ insertSchema: insertTaskSchema,
715
+ updateSchema: updateTaskSchema,
716
+
717
+ middleware: {
718
+ // 简单副作用:只在操作后执行
719
+ create: afterMiddleware(async (ctx, result) => {
720
+ await sendEmail(result);
721
+ await logAudit(ctx.user, "create", result);
722
+ }),
723
+
724
+ // 修改输入:只在操作前执行
725
+ update: beforeMiddleware(async (ctx, data) => {
726
+ return { ...data, updatedAt: new Date(), updatedBy: ctx.user.id };
727
+ }),
728
+
729
+ // 类型安全的便捷函数
730
+ delete: afterDelete(async (ctx, deleted) => {
731
+ await cleanupRelatedData(deleted.id);
732
+ }),
733
+ },
734
+ });
735
+ ```
736
+
737
+ ### 可用的工具函数
738
+
739
+ | 函数 | 用途 | 示例 |
740
+ |------|------|------|
741
+ | `afterMiddleware(fn)` | 操作后执行副作用 | 日志、通知、审计 |
742
+ | `afterMiddlewareTransform(fn)` | 操作后修改返回值 | 添加计算字段 |
743
+ | `beforeMiddleware(fn)` | 操作前修改输入 | 注入用户ID、生成slug |
744
+ | `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
745
+ | `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
746
+ | `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
747
+ | `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
748
+
749
+ ---
750
+
611
751
  ## 🔌 与其他库集成
612
752
 
613
753
  ### 与 Drizzle ORM 集成
@@ -698,7 +838,26 @@ export const filterVariants = {
698
838
  ```typescript
699
839
  // 主要导出
700
840
  export { createCrudRouter } from "./routers/_factory";
701
- export type { CrudRouterConfig } from "./routers/_factory";
841
+ export type {
842
+ CrudRouterConfig,
843
+ CrudMiddleware,
844
+ ListInput,
845
+ ListResult,
846
+ } from "./types/config";
847
+
848
+ // Middleware 工具函数
849
+ export {
850
+ afterMiddleware,
851
+ afterMiddlewareTransform,
852
+ beforeMiddleware,
853
+ composeMiddleware,
854
+ afterList,
855
+ beforeList,
856
+ afterCreate,
857
+ beforeCreate,
858
+ afterUpdate,
859
+ afterDelete,
860
+ } from "./lib/middleware-helpers";
702
861
 
703
862
  // tRPC 工具
704
863
  export { router, publicProcedure } from "./trpc";
package/dist/index.cjs CHANGED
@@ -474,11 +474,19 @@ function validateColumn(columnId, allowedColumns) {
474
474
  if (!allowedColumns || allowedColumns.length === 0) return true;
475
475
  return allowedColumns.includes(columnId);
476
476
  }
477
+ /**
478
+ * 创建 CRUD Router
479
+ *
480
+ * @typeParam TContext - 上下文类型(包含 db)
481
+ * @typeParam TSelect - 查询返回类型(从 selectSchema 或 insertSchema 推断)
482
+ * @typeParam TInsert - 创建输入类型(从 insertSchema 推断)
483
+ * @typeParam TUpdate - 更新输入类型(从 updateSchema 推断)
484
+ */
477
485
  function createCrudRouter(config) {
478
- const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, hooks = {} } = config;
486
+ const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
479
487
  const resolved = resolveConfig(config);
480
488
  const softDelete = resolveSoftDelete(softDeleteOption);
481
- const h = hooks;
489
+ const m = middleware;
482
490
  const getIdColumn = () => table[idField];
483
491
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
484
492
  const buildWhere = (ctx, operation, additionalCondition) => {
@@ -496,6 +504,12 @@ function createCrudRouter(config) {
496
504
  };
497
505
  const withGuard = async (ctx, operation) => {
498
506
  if (resolved.guard) {
507
+ if (operation === "upsert") {
508
+ const canCreate = await resolved.guard(ctx, "create");
509
+ const canUpdate = await resolved.guard(ctx, "update");
510
+ if (!canCreate || !canUpdate) throw new Error("Forbidden: upsert requires both create and update permissions");
511
+ return;
512
+ }
499
513
  if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
500
514
  }
501
515
  };
@@ -506,72 +520,75 @@ function createCrudRouter(config) {
506
520
  const procedures = {
507
521
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
508
522
  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));
523
+ const doList = async (listInput$1) => {
524
+ const offset = (listInput$1.page - 1) * listInput$1.perPage;
525
+ const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
526
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
527
+ table,
528
+ filters: validatedFilters,
529
+ joinOperator: listInput$1.joinOperator
530
+ }) : void 0);
531
+ let query = ctx.db.select().from(table).$dynamic();
532
+ if (where) query = query.where(where);
533
+ if (listInput$1.sort?.length) {
534
+ const sortField = listInput$1.sort[0];
535
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
536
+ const column = table[sortField.id];
537
+ if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
538
+ }
528
539
  }
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)
540
+ const data = await query.limit(listInput$1.perPage).offset(offset);
541
+ let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
542
+ if (where) countQuery = countQuery.where(where);
543
+ const count = (await countQuery)[0]?.count ?? 0;
544
+ return {
545
+ data,
546
+ total: count,
547
+ page: listInput$1.page,
548
+ perPage: listInput$1.perPage,
549
+ pageCount: Math.ceil(count / listInput$1.perPage)
550
+ };
540
551
  };
541
- if (h.afterList) {
542
- const result = await h.afterList(ctx, listResult);
543
- if (result) listResult = result;
544
- }
545
- return listResult;
552
+ const listInput = input;
553
+ if (m.list) return m.list({
554
+ ctx,
555
+ input: listInput,
556
+ next: async (modifiedInput) => doList(modifiedInput ?? listInput)
557
+ });
558
+ return doList(listInput);
546
559
  }),
547
- getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
560
+ get: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
548
561
  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;
562
+ const doGet = async () => {
563
+ const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
564
+ const [item] = await ctx.db.select().from(table).where(where);
565
+ await withAuthorize(ctx, item, "get");
566
+ return item ?? null;
567
+ };
568
+ if (m.get) return m.get({
569
+ ctx,
570
+ id: input,
571
+ next: doGet
572
+ });
573
+ return doGet();
559
574
  }),
560
575
  create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
561
576
  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
577
+ const doCreate = async (inputData) => {
578
+ const injectData = resolved.getInject(ctx, "create");
579
+ const data = {
580
+ ...inputData,
581
+ ...injectData
582
+ };
583
+ const [created] = await ctx.db.insert(table).values(data).returning();
584
+ return created;
571
585
  };
572
- const [created] = await ctx.db.insert(table).values(data).returning();
573
- if (h.afterCreate) await h.afterCreate(ctx, created);
574
- return created;
586
+ if (m.create) return m.create({
587
+ ctx,
588
+ input,
589
+ next: async (modifiedInput) => doCreate(modifiedInput ?? input)
590
+ });
591
+ return doCreate(input);
575
592
  }),
576
593
  update: resolved.procedureFactory("update").input(zod.z.object({
577
594
  id: zod.z.string(),
@@ -582,19 +599,23 @@ function createCrudRouter(config) {
582
599
  const [existing] = await ctx.db.select().from(table).where(where);
583
600
  await withAuthorize(ctx, existing, "update");
584
601
  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
602
+ const doUpdate = async (updateData) => {
603
+ const injectData = resolved.getInject(ctx, "update");
604
+ const data = {
605
+ ...updateData,
606
+ ...injectData
607
+ };
608
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
609
+ return updated;
594
610
  };
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;
611
+ if (m.update) return m.update({
612
+ ctx,
613
+ id: input.id,
614
+ data: input.data,
615
+ existing,
616
+ next: async (modifiedData) => doUpdate(modifiedData ?? input.data)
617
+ });
618
+ return doUpdate(input.data);
598
619
  }),
599
620
  delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
600
621
  await withGuard(ctx, "delete");
@@ -602,54 +623,169 @@ function createCrudRouter(config) {
602
623
  const [existing] = await ctx.db.select().from(table).where(where);
603
624
  await withAuthorize(ctx, existing, "delete");
604
625
  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;
626
+ const doDelete = async () => {
627
+ let deleted;
628
+ if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
629
+ else [deleted] = await ctx.db.delete(table).where(where).returning();
630
+ return deleted;
631
+ };
632
+ if (m.delete) return m.delete({
633
+ ctx,
634
+ id: input,
635
+ existing,
636
+ next: doDelete
637
+ });
638
+ return doDelete();
611
639
  }),
612
640
  deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
613
641
  await withGuard(ctx, "deleteMany");
614
- if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
615
642
  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;
643
+ const doDeleteMany = async () => {
644
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
645
+ else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
646
+ };
647
+ if (m.deleteMany) return m.deleteMany({
648
+ ctx,
649
+ ids: input,
650
+ next: doDeleteMany
651
+ });
652
+ return doDeleteMany();
621
653
  }),
622
654
  updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
623
655
  ids: zod.z.array(zod.z.string()).max(maxBatchSize),
624
656
  data: updateSchema
625
657
  })).mutation(async ({ ctx, input }) => {
626
658
  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
659
  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
660
+ const doUpdateMany = async (updateData) => {
661
+ const injectData = resolved.getInject(ctx, "update");
662
+ const data = {
663
+ ...updateData,
664
+ ...injectData
665
+ };
666
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
667
+ };
668
+ if (m.updateMany) return m.updateMany({
669
+ ctx,
670
+ ids: input.ids,
671
+ data: input.data,
672
+ next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
673
+ });
674
+ return doUpdateMany(input.data);
675
+ }),
676
+ upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
677
+ await withGuard(ctx, "upsert");
678
+ const doUpsert = async (inputData) => {
679
+ const injectData = resolved.getInject(ctx, "create");
680
+ const data = {
681
+ ...inputData,
682
+ ...injectData
683
+ };
684
+ const inputId = inputData[idField];
685
+ let isNew = true;
686
+ if (inputId) {
687
+ const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
688
+ const [existing] = await ctx.db.select().from(table).where(where);
689
+ isNew = !existing;
690
+ }
691
+ const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
692
+ target: getIdColumn(),
693
+ set: data
694
+ }).returning();
695
+ return {
696
+ data: result,
697
+ isNew
698
+ };
637
699
  };
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;
700
+ if (m.upsert) return m.upsert({
701
+ ctx,
702
+ input,
703
+ next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
704
+ });
705
+ return doUpsert(input);
641
706
  })
642
707
  };
643
708
  const crudRouter = router(procedures);
644
709
  return Object.assign(crudRouter, { procedures });
645
710
  }
646
711
 
712
+ //#endregion
713
+ //#region src/lib/middleware-helpers.ts
714
+ /**
715
+ * Middleware 便捷工具函数
716
+ * 简化常见场景的 middleware 创建
717
+ */
718
+ /**
719
+ * 创建 after-only 的 middleware(最常见场景)
720
+ * 在操作完成后执行副作用,不修改返回值
721
+ *
722
+ * @example
723
+ * middleware: {
724
+ * create: afterMiddleware(async (ctx, result) => {
725
+ * await sendEmail(result);
726
+ * await logAudit(ctx.user, 'create', result);
727
+ * }),
728
+ * }
729
+ */
730
+ function afterMiddleware(fn) {
731
+ return async (params) => {
732
+ const result = await params.next();
733
+ await fn(params.ctx, result);
734
+ return result;
735
+ };
736
+ }
737
+ /**
738
+ * 创建 before-only 的 middleware
739
+ * 在操作执行前修改输入数据
740
+ *
741
+ * @example
742
+ * middleware: {
743
+ * create: beforeMiddleware(async (ctx, input) => {
744
+ * return { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
745
+ * }),
746
+ * }
747
+ */
748
+ function beforeMiddleware(fn) {
749
+ return async (params) => {
750
+ const modifiedInput = await fn(params.ctx, params.input);
751
+ return params.next(modifiedInput);
752
+ };
753
+ }
754
+ /**
755
+ * 组合多个 middleware 为一个
756
+ *
757
+ * @example
758
+ * middleware: {
759
+ * create: composeMiddleware(
760
+ * beforeMiddleware((ctx, input) => ({ ...input, slug: slugify(input.title) })),
761
+ * afterMiddleware((ctx, result) => sendEmail(result)),
762
+ * ),
763
+ * }
764
+ */
765
+ function composeMiddleware(...middlewares) {
766
+ return async (params) => {
767
+ let index = 0;
768
+ const executeNext = async (...args) => {
769
+ if (index >= middlewares.length) return params.next(...args);
770
+ const currentMiddleware = middlewares[index++];
771
+ return currentMiddleware({
772
+ ...params,
773
+ next: executeNext
774
+ });
775
+ };
776
+ return executeNext();
777
+ };
778
+ }
779
+
647
780
  //#endregion
648
781
  //#region src/routers/index.ts
649
782
  const appRouter = router({});
650
783
 
651
784
  //#endregion
785
+ exports.afterMiddleware = afterMiddleware;
652
786
  exports.appRouter = appRouter;
787
+ exports.beforeMiddleware = beforeMiddleware;
788
+ exports.composeMiddleware = composeMiddleware;
653
789
  exports.createCrudRouter = createCrudRouter;
654
790
  exports.publicProcedure = publicProcedure;
655
791
  exports.router = router;