@wordrhyme/auto-crud-server 0.6.0 → 0.6.2

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
  ## 🎯 高级过滤
package/dist/index.cjs CHANGED
@@ -474,6 +474,14 @@ 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
486
  const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
479
487
  const resolved = resolveConfig(config);
@@ -485,7 +493,7 @@ function createCrudRouter(config) {
485
493
  const conditions = [];
486
494
  const scopeCondition = resolved.getScope(ctx, table, operation);
487
495
  if (scopeCondition) conditions.push(scopeCondition);
488
- if (softDelete && (operation === "list" || operation === "get")) {
496
+ if (softDelete) {
489
497
  const col = getSoftDeleteColumn();
490
498
  if (col) conditions.push((0, drizzle_orm.isNull)(col));
491
499
  }
@@ -496,12 +504,27 @@ function createCrudRouter(config) {
496
504
  };
497
505
  const withGuard = async (ctx, operation) => {
498
506
  if (resolved.guard) {
499
- if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
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 __trpc_server.TRPCError({
511
+ code: "FORBIDDEN",
512
+ message: "Forbidden: upsert requires both create and update permissions"
513
+ });
514
+ return;
515
+ }
516
+ if (!await resolved.guard(ctx, operation)) throw new __trpc_server.TRPCError({
517
+ code: "FORBIDDEN",
518
+ message: `Forbidden: ${operation} not allowed`
519
+ });
500
520
  }
501
521
  };
502
522
  const withAuthorize = async (ctx, resource, operation) => {
503
523
  if (!resource) return;
504
- if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
524
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new __trpc_server.TRPCError({
525
+ code: "FORBIDDEN",
526
+ message: `Forbidden: Cannot ${operation} this resource`
527
+ });
505
528
  };
506
529
  const procedures = {
507
530
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
@@ -543,7 +566,7 @@ function createCrudRouter(config) {
543
566
  });
544
567
  return doList(listInput);
545
568
  }),
546
- getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
569
+ get: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
547
570
  await withGuard(ctx, "get");
548
571
  const doGet = async () => {
549
572
  const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
@@ -584,7 +607,10 @@ function createCrudRouter(config) {
584
607
  const where = buildWhere(ctx, "update", (0, drizzle_orm.eq)(getIdColumn(), input.id));
585
608
  const [existing] = await ctx.db.select().from(table).where(where);
586
609
  await withAuthorize(ctx, existing, "update");
587
- if (!existing) throw new Error("Resource not found or access denied");
610
+ if (!existing) throw new __trpc_server.TRPCError({
611
+ code: "NOT_FOUND",
612
+ message: "Resource not found or access denied"
613
+ });
588
614
  const doUpdate = async (updateData) => {
589
615
  const injectData = resolved.getInject(ctx, "update");
590
616
  const data = {
@@ -608,7 +634,10 @@ function createCrudRouter(config) {
608
634
  const where = buildWhere(ctx, "delete", (0, drizzle_orm.eq)(getIdColumn(), input));
609
635
  const [existing] = await ctx.db.select().from(table).where(where);
610
636
  await withAuthorize(ctx, existing, "delete");
611
- if (!existing) throw new Error("Resource not found or access denied");
637
+ if (!existing) throw new __trpc_server.TRPCError({
638
+ code: "NOT_FOUND",
639
+ message: "Resource not found or access denied"
640
+ });
612
641
  const doDelete = async () => {
613
642
  let deleted;
614
643
  if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
@@ -627,8 +656,8 @@ function createCrudRouter(config) {
627
656
  await withGuard(ctx, "deleteMany");
628
657
  const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
629
658
  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 };
659
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
660
+ else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
632
661
  };
633
662
  if (m.deleteMany) return m.deleteMany({
634
663
  ctx,
@@ -649,7 +678,7 @@ function createCrudRouter(config) {
649
678
  ...updateData,
650
679
  ...injectData
651
680
  };
652
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
681
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
653
682
  };
654
683
  if (m.updateMany) return m.updateMany({
655
684
  ctx,
@@ -658,6 +687,43 @@ function createCrudRouter(config) {
658
687
  next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
659
688
  });
660
689
  return doUpdateMany(input.data);
690
+ }),
691
+ upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
692
+ await withGuard(ctx, "upsert");
693
+ const doUpsert = async (inputData) => {
694
+ const inputId = inputData[idField];
695
+ let isNew = true;
696
+ let existing = null;
697
+ if (inputId) {
698
+ const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
699
+ const [found] = await ctx.db.select().from(table).where(where);
700
+ existing = found;
701
+ isNew = !existing;
702
+ if (!isNew && existing) await withAuthorize(ctx, existing, "update");
703
+ }
704
+ const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
705
+ const data = {
706
+ ...inputData,
707
+ ...injectData
708
+ };
709
+ const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
710
+ target: getIdColumn(),
711
+ set: {
712
+ ...inputData,
713
+ ...resolved.getInject(ctx, "update")
714
+ }
715
+ }).returning();
716
+ return {
717
+ data: result,
718
+ isNew
719
+ };
720
+ };
721
+ if (m.upsert) return m.upsert({
722
+ ctx,
723
+ input,
724
+ next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
725
+ });
726
+ return doUpsert(input);
661
727
  })
662
728
  };
663
729
  const crudRouter = router(procedures);
@@ -667,6 +733,10 @@ function createCrudRouter(config) {
667
733
  //#endregion
668
734
  //#region src/lib/middleware-helpers.ts
669
735
  /**
736
+ * Middleware 便捷工具函数
737
+ * 简化常见场景的 middleware 创建
738
+ */
739
+ /**
670
740
  * 创建 after-only 的 middleware(最常见场景)
671
741
  * 在操作完成后执行副作用,不修改返回值
672
742
  *
@@ -679,28 +749,13 @@ function createCrudRouter(config) {
679
749
  * }
680
750
  */
681
751
  function afterMiddleware(fn) {
682
- return async ({ ctx, next }) => {
683
- const result = await next();
684
- await fn(ctx, result);
752
+ return async (params) => {
753
+ const result = await params.next();
754
+ await fn(params.ctx, result);
685
755
  return result;
686
756
  };
687
757
  }
688
758
  /**
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
759
  * 创建 before-only 的 middleware
705
760
  * 在操作执行前修改输入数据
706
761
  *
@@ -712,8 +767,9 @@ function afterMiddlewareTransform(fn) {
712
767
  * }
713
768
  */
714
769
  function beforeMiddleware(fn) {
715
- return async ({ ctx, input, next }) => {
716
- return next(await fn(ctx, input));
770
+ return async (params) => {
771
+ const modifiedInput = await fn(params.ctx, params.input);
772
+ return params.next(modifiedInput);
717
773
  };
718
774
  }
719
775
  /**
@@ -730,69 +786,25 @@ function beforeMiddleware(fn) {
730
786
  function composeMiddleware(...middlewares) {
731
787
  return async (params) => {
732
788
  let index = 0;
733
- const executeNext = async (input) => {
734
- if (index >= middlewares.length) return params.next(input);
789
+ const executeNext = async (...args) => {
790
+ if (index >= middlewares.length) return params.next(...args);
735
791
  const currentMiddleware = middlewares[index++];
736
792
  return currentMiddleware({
737
- ctx: params.ctx,
738
- input,
793
+ ...params,
739
794
  next: executeNext
740
795
  });
741
796
  };
742
- return executeNext(params.input);
797
+ return executeNext();
743
798
  };
744
799
  }
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
800
 
782
801
  //#endregion
783
802
  //#region src/routers/index.ts
784
803
  const appRouter = router({});
785
804
 
786
805
  //#endregion
787
- exports.afterCreate = afterCreate;
788
- exports.afterDelete = afterDelete;
789
- exports.afterList = afterList;
790
806
  exports.afterMiddleware = afterMiddleware;
791
- exports.afterMiddlewareTransform = afterMiddlewareTransform;
792
- exports.afterUpdate = afterUpdate;
793
807
  exports.appRouter = appRouter;
794
- exports.beforeCreate = beforeCreate;
795
- exports.beforeList = beforeList;
796
808
  exports.beforeMiddleware = beforeMiddleware;
797
809
  exports.composeMiddleware = composeMiddleware;
798
810
  exports.createCrudRouter = createCrudRouter;