@wordrhyme/auto-crud-server 0.8.0 → 0.10.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
@@ -75,27 +75,32 @@ export const tasks = pgTable("tasks", {
75
75
  // src/server/routers/tasks.ts
76
76
  import { createCrudRouter } from "@wordrhyme/auto-crud-server";
77
77
  import { tasks } from "@/db/schema";
78
+
79
+ // 🚀 零配置!一行代码生成完整 CRUD 路由
80
+ export const tasksRouter = createCrudRouter({
81
+ table: tasks,
82
+ // Schema 自动从 table 派生,排除 id, createdAt, updatedAt
83
+ });
84
+ ```
85
+
86
+ **或者,显式传入 Schema:**
87
+
88
+ ```typescript
89
+ import { createCrudRouter } from "@wordrhyme/auto-crud-server";
90
+ import { tasks } from "@/db/schema";
78
91
  import { createSelectSchema } from "drizzle-zod";
79
92
 
80
93
  // 从 Drizzle Schema 自动生成 Zod Schema
81
- const selectTaskSchema = createSelectSchema(tasks);
82
-
83
- // 创建输入 Schema
84
- const insertTaskSchema = selectTaskSchema.omit({
94
+ const taskSchema = createSelectSchema(tasks).omit({
85
95
  id: true,
86
96
  createdAt: true,
87
97
  updatedAt: true,
88
98
  });
89
99
 
90
- const updateTaskSchema = insertTaskSchema.partial();
91
-
92
- // 🚀 一行代码生成完整 CRUD 路由
93
100
  export const tasksRouter = createCrudRouter({
94
101
  table: tasks,
95
- selectSchema: selectTaskSchema,
96
- insertSchema: insertTaskSchema,
97
- updateSchema: updateTaskSchema,
98
- idField: "id", // 可选,默认为 "id"
102
+ schema: taskSchema, // 主 Schema(用于 create/upsert)
103
+ // updateSchema 自动派生为 schema.partial()
99
104
  });
100
105
  ```
101
106
 
@@ -440,14 +445,57 @@ await trpc.tasks.list({
440
445
 
441
446
  ```typescript
442
447
  interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
448
+ // ========== 必填 ==========
443
449
  table: TTable; // Drizzle 表定义
444
- selectSchema: z.ZodType<TSelect>; // 查询返回 Schema
445
- insertSchema: z.ZodType<TInsert>; // 创建输入 Schema
446
- updateSchema: z.ZodType<TUpdate>; // 更新输入 Schema
450
+
451
+ // ========== Schema 配置(可选) ==========
452
+ schema?: z.ZodType<TInsert>; // Schema(用于 create/upsert)
453
+ updateSchema?: z.ZodType<TUpdate>;// 更新 Schema(覆盖自动派生)
454
+ selectSchema?: z.ZodType<TSelect>;// 查询返回 Schema
455
+
456
+ // ========== 其他配置 ==========
447
457
  idField?: string; // ID 字段名,默认 "id"
458
+ omitFields?: string[]; // 自动派生时排除的字段
459
+ // 默认 ["id", "createdAt", "updatedAt"]
448
460
  }
449
461
  ```
450
462
 
463
+ #### Schema 派生规则
464
+
465
+ | 配置 | 派生行为 |
466
+ |-----|---------|
467
+ | 无 `schema` | 从 `table` 自动派生,排除 `omitFields` |
468
+ | 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
469
+ | 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
470
+
471
+ #### 使用示例
472
+
473
+ ```typescript
474
+ // 1. 零配置 - 全部自动派生
475
+ const tasksRouter = createCrudRouter({
476
+ table: tasks,
477
+ });
478
+
479
+ // 2. 传入 schema,updateSchema 自动派生
480
+ const postsRouter = createCrudRouter({
481
+ table: posts,
482
+ schema: postSchema,
483
+ });
484
+
485
+ // 3. 传入 schema + 自定义 updateSchema
486
+ const usersRouter = createCrudRouter({
487
+ table: users,
488
+ schema: userSchema,
489
+ updateSchema: customUpdateSchema,
490
+ });
491
+
492
+ // 4. 自定义排除字段
493
+ const ordersRouter = createCrudRouter({
494
+ table: orders,
495
+ omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
496
+ });
497
+ ```
498
+
451
499
  #### 返回值
452
500
 
453
501
  ```typescript
@@ -459,95 +507,94 @@ interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
459
507
  // 可 spread 的 procedures 对象(用于扩展自定义路由)
460
508
  procedures: {
461
509
  list: Procedure<ListInput, ListOutput>,
462
- getById: Procedure<string, TSelect>,
510
+ get: Procedure<string, TSelect>,
463
511
  create: Procedure<TInsert, TSelect>,
464
512
  update: Procedure<{ id: string, data: TUpdate }, TSelect>,
465
513
  delete: Procedure<string, TSelect>,
466
514
  deleteMany: Procedure<string[], { deleted: number }>,
467
515
  updateMany: Procedure<{ ids: string[], data: TUpdate }, { updated: number }>,
516
+ upsert: Procedure<TInsert, { data: TSelect, isNew: boolean }>,
468
517
  }
469
518
  }
470
519
  ```
471
520
 
472
- #### 完整示例
521
+ ---
522
+
523
+ ## 🔐 权限控制
524
+
525
+ ### 使用 procedure 配置
473
526
 
474
527
  ```typescript
475
528
  import { createCrudRouter } from "@wordrhyme/auto-crud-server";
476
- import { users } from "@/db/schema";
477
- import { createSelectSchema } from "drizzle-zod";
478
- import { z } from "zod";
529
+ import { protectedProcedure, adminProcedure, publicProcedure } from "../trpc";
479
530
 
480
- const selectUserSchema = createSelectSchema(users);
481
-
482
- const insertUserSchema = selectUserSchema.omit({
483
- id: true,
484
- createdAt: true,
485
- updatedAt: true,
531
+ export const tasksRouter = createCrudRouter({
532
+ table: tasks,
533
+ // 按操作指定不同的 procedure
534
+ procedure: {
535
+ list: publicProcedure, // 公开读
536
+ get: publicProcedure,
537
+ create: protectedProcedure, // 需要登录
538
+ update: protectedProcedure,
539
+ delete: adminProcedure, // 需要管理员
540
+ default: protectedProcedure,
541
+ },
486
542
  });
543
+ ```
487
544
 
488
- const updateUserSchema = insertUserSchema.partial();
545
+ ### 使用 guard(操作级守卫)
489
546
 
490
- export const usersRouter = createCrudRouter({
491
- table: users,
492
- selectSchema: selectUserSchema,
493
- insertSchema: insertUserSchema,
494
- updateSchema: updateUserSchema,
495
- idField: "id",
547
+ ```typescript
548
+ export const tasksRouter = createCrudRouter({
549
+ table: tasks,
550
+ guard: (ctx, operation) => {
551
+ // 删除操作需要管理员权限
552
+ if (operation === "delete") {
553
+ return ctx.user.role === "admin";
554
+ }
555
+ // 其他操作需要登录
556
+ return !!ctx.user;
557
+ },
496
558
  });
497
559
  ```
498
560
 
499
- ---
500
-
501
- ## 🔐 权限控制
502
-
503
- ### 使用 tRPC Middleware
561
+ ### 使用 scope(行级过滤 RLS)
504
562
 
505
563
  ```typescript
506
- import { initTRPC, TRPCError } from "@trpc/server";
507
-
508
- const t = initTRPC.context<Context>().create();
564
+ import { eq } from "drizzle-orm";
509
565
 
510
- // 创建受保护的 procedure
511
- const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
512
- if (!ctx.user) {
513
- throw new TRPCError({ code: "UNAUTHORIZED" });
514
- }
515
- return next({ ctx: { ...ctx, user: ctx.user } });
566
+ export const tasksRouter = createCrudRouter({
567
+ table: tasks,
568
+ // 自动注入到所有查询的 WHERE 子句
569
+ scope: (ctx, table, operation) => {
570
+ return eq(table.tenantId, ctx.user.tenantId);
571
+ },
516
572
  });
573
+ ```
574
+
575
+ ### 使用 authorize(资源级检查 ABAC)
517
576
 
518
- // 在 createCrudRouter 中使用
577
+ ```typescript
519
578
  export const tasksRouter = createCrudRouter({
520
579
  table: tasks,
521
- selectSchema: selectTaskSchema,
522
- insertSchema: insertTaskSchema,
523
- updateSchema: updateTaskSchema,
524
- // 注意:当前版本不支持直接传入 procedure
525
- // 需要在路由层面添加权限控制
580
+ // 在 get/update/delete 时,预取资源后调用
581
+ authorize: (ctx, resource, operation) => {
582
+ return resource.ownerId === ctx.user.id;
583
+ },
526
584
  });
527
-
528
- // 包装路由添加权限
529
- export const protectedTasksRouter = {
530
- list: protectedProcedure.query(async ({ input, ctx }) => {
531
- return tasksRouter.list(input, ctx);
532
- }),
533
- // ... 其他路由
534
- };
535
585
  ```
536
586
 
537
- ### 行级权限控制
587
+ ### 使用 inject(强制注入字段)
538
588
 
539
589
  ```typescript
540
- // createContext 中添加用户信息
541
- export const createContext = async ({ req }: { req: Request }) => {
542
- const user = await getUserFromRequest(req);
543
- return { db, user };
544
- };
545
-
546
- // 在查询中添加过滤条件
547
- const result = await db
548
- .select()
549
- .from(tasks)
550
- .where(eq(tasks.userId, ctx.user.id)); // 只查询当前用户的任务
590
+ export const tasksRouter = createCrudRouter({
591
+ table: tasks,
592
+ // 写入时强制覆盖字段(防止伪造)
593
+ inject: (ctx, operation) => ({
594
+ tenantId: ctx.user.tenantId,
595
+ ...(operation === "create" ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
596
+ }),
597
+ });
551
598
  ```
552
599
 
553
600
  ---
@@ -566,9 +613,6 @@ import { z } from "zod";
566
613
  // 创建基础 CRUD
567
614
  const tasksCrud = createCrudRouter({
568
615
  table: tasks,
569
- selectSchema: selectTaskSchema,
570
- insertSchema: insertTaskSchema,
571
- updateSchema: updateTaskSchema,
572
616
  });
573
617
 
574
618
  // 使用 .procedures spread 并添加自定义路由
@@ -604,9 +648,6 @@ export const tasksRouter = router({
604
648
  ```typescript
605
649
  const tasksCrud = createCrudRouter({
606
650
  table: tasks,
607
- selectSchema: selectTaskSchema,
608
- insertSchema: insertTaskSchema,
609
- updateSchema: updateTaskSchema,
610
651
  });
611
652
 
612
653
  // 直接嵌套到 appRouter
@@ -619,7 +660,7 @@ export const appRouter = router({
619
660
 
620
661
  ```typescript
621
662
  // 覆盖 list 路由,添加自定义过滤逻辑
622
- const tasksCrud = createCrudRouter({ ... });
663
+ const tasksCrud = createCrudRouter({ table: tasks });
623
664
 
624
665
  export const tasksRouter = router({
625
666
  ...tasksCrud.procedures,
@@ -655,8 +696,6 @@ import { createCrudRouter } from "@wordrhyme/auto-crud-server";
655
696
 
656
697
  const tasksRouter = createCrudRouter({
657
698
  table: tasks,
658
- insertSchema: insertTaskSchema,
659
- updateSchema: updateTaskSchema,
660
699
 
661
700
  // 中间件:完全控制操作流程
662
701
  middleware: {
@@ -711,8 +750,6 @@ import {
711
750
 
712
751
  const tasksRouter = createCrudRouter({
713
752
  table: tasks,
714
- insertSchema: insertTaskSchema,
715
- updateSchema: updateTaskSchema,
716
753
 
717
754
  middleware: {
718
755
  // 简单副作用:只在操作后执行
@@ -811,24 +848,53 @@ app.listen(3000);
811
848
  ```typescript
812
849
  export const usersRouter = createCrudRouter({
813
850
  table: users,
814
- selectSchema: selectUserSchema,
815
- insertSchema: insertUserSchema,
816
- updateSchema: updateUserSchema,
817
851
  idField: "userId", // 使用自定义 ID 字段
818
852
  });
819
853
  ```
820
854
 
821
- ### 自定义过滤器配置
855
+ ### 自定义排除字段
822
856
 
823
857
  ```typescript
824
- // src/config/data-table.ts
825
- export const filterVariants = {
826
- text: ["eq", "ne", "like", "notLike"],
827
- number: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
828
- select: ["eq", "ne", "in", "notIn"],
829
- date: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
830
- boolean: ["eq"],
831
- };
858
+ export const ordersRouter = createCrudRouter({
859
+ table: orders,
860
+ // 自动派生 schema 时排除这些字段
861
+ omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
862
+ });
863
+ ```
864
+
865
+ ### 软删除
866
+
867
+ ```typescript
868
+ export const tasksRouter = createCrudRouter({
869
+ table: tasks,
870
+ // 方式 1:使用默认列名 'deletedAt'
871
+ softDelete: true,
872
+
873
+ // 方式 2:指定列名
874
+ softDelete: "deletedAt",
875
+
876
+ // 方式 3:完整配置(用于布尔字段)
877
+ softDelete: { column: "isDeleted", value: () => true },
878
+ });
879
+ ```
880
+
881
+ ### 批量操作限制
882
+
883
+ ```typescript
884
+ export const tasksRouter = createCrudRouter({
885
+ table: tasks,
886
+ maxBatchSize: 50, // 默认 100
887
+ });
888
+ ```
889
+
890
+ ### 列白名单
891
+
892
+ ```typescript
893
+ export const tasksRouter = createCrudRouter({
894
+ table: tasks,
895
+ filterableColumns: ["title", "status", "priority"], // 只允许这些列过滤
896
+ sortableColumns: ["title", "createdAt", "priority"], // 只允许这些列排序
897
+ });
832
898
  ```
833
899
 
834
900
  ---
package/dist/index.cjs CHANGED
@@ -25,6 +25,8 @@ let zod = require("zod");
25
25
  zod = __toESM(zod);
26
26
  let drizzle_orm = require("drizzle-orm");
27
27
  drizzle_orm = __toESM(drizzle_orm);
28
+ let drizzle_zod = require("drizzle-zod");
29
+ drizzle_zod = __toESM(drizzle_zod);
28
30
  let __trpc_server = require("@trpc/server");
29
31
  __trpc_server = __toESM(__trpc_server);
30
32
  let superjson = require("superjson");
@@ -399,6 +401,26 @@ const dataTableConfig = {
399
401
  joinOperators: ["and", "or"]
400
402
  };
401
403
 
404
+ //#endregion
405
+ //#region src/lib/schema-utils.ts
406
+ /**
407
+ * 检查是否为 ZodObject 类型
408
+ * 兼容 Zod v3 和 v4
409
+ *
410
+ * 实现策略:检查 schema 是否具有 ZodObject 特有的 .shape 属性
411
+ * 这种方式比检查内部 _def 属性更稳定,因为不依赖 Zod 的内部实现细节
412
+ */
413
+ function isZodObject(schema) {
414
+ return schema !== null && typeof schema === "object" && "shape" in schema && typeof schema.shape === "object" && schema.shape !== null;
415
+ }
416
+ /**
417
+ * 非空对象验证器
418
+ * 用于 updateSchema 的 refine,防止空更新
419
+ */
420
+ function nonEmpty(value) {
421
+ return Object.keys(value).length > 0;
422
+ }
423
+
402
424
  //#endregion
403
425
  //#region src/routers/_factory.ts
404
426
  function resolveSoftDelete(option) {
@@ -434,6 +456,9 @@ function isProcedureMap(config) {
434
456
  "deleteMany",
435
457
  "updateMany",
436
458
  "upsert",
459
+ "export",
460
+ "import",
461
+ "createMany",
437
462
  "default"
438
463
  ];
439
464
  return keys.some((k) => validKeys.includes(k));
@@ -485,20 +510,63 @@ const listInputSchema = zod.z.object({
485
510
  filters: zod.z.array(filterItemSchema).optional(),
486
511
  joinOperator: zod.z.enum(["and", "or"]).default("and")
487
512
  });
513
+ const exportInputSchema = zod.z.object({
514
+ sort: zod.z.array(zod.z.object({
515
+ id: zod.z.string(),
516
+ desc: zod.z.boolean()
517
+ })).optional(),
518
+ filters: zod.z.array(filterItemSchema).optional(),
519
+ joinOperator: zod.z.enum(["and", "or"]).default("and"),
520
+ limit: zod.z.number().min(1).optional()
521
+ });
522
+ const importInputSchema = zod.z.object({
523
+ rows: zod.z.array(zod.z.unknown()).min(1).max(1e3),
524
+ onConflict: zod.z.enum([
525
+ "skip",
526
+ "upsert",
527
+ "error"
528
+ ]).default("skip")
529
+ });
530
+ const DEFAULT_OMIT_FIELDS = [
531
+ "id",
532
+ "createdAt",
533
+ "updatedAt"
534
+ ];
535
+ /**
536
+ * 解析并派生 Schema
537
+ * - 优先使用显式传入的 Schema
538
+ * - 否则从 Drizzle table 自动派生
539
+ */
540
+ function resolveSchemas(config) {
541
+ const { table, omitFields = DEFAULT_OMIT_FIELDS } = config;
542
+ let schema;
543
+ if (config.schema) {
544
+ if (!isZodObject(config.schema)) throw new Error("[createCrudRouter] schema must be a ZodObject (not ZodEffects or other types). If you're using .refine() or .transform(), please remove them from schema.");
545
+ schema = config.schema;
546
+ } else {
547
+ const rawSchema = (0, drizzle_zod.createInsertSchema)(table);
548
+ if (!isZodObject(rawSchema)) throw new Error("[createCrudRouter] drizzle-zod returned non-ZodObject schema. This may be due to table refinements. Please provide schema explicitly.");
549
+ const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
550
+ schema = rawSchema.omit(omitConfig);
551
+ }
552
+ let selectSchema;
553
+ if (config.selectSchema) selectSchema = config.selectSchema;
554
+ else if (config.schema) selectSchema = schema;
555
+ else selectSchema = (0, drizzle_zod.createSelectSchema)(table);
556
+ const updateSchema = config.updateSchema ?? schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
557
+ return {
558
+ selectSchema,
559
+ schema,
560
+ updateSchema
561
+ };
562
+ }
488
563
  function validateColumn(columnId, allowedColumns) {
489
564
  if (!allowedColumns || allowedColumns.length === 0) return true;
490
565
  return allowedColumns.includes(columnId);
491
566
  }
492
- /**
493
- * 创建 CRUD Router
494
- *
495
- * @typeParam TContext - 上下文类型(包含 db)
496
- * @typeParam TSelect - 查询返回类型(从 selectSchema 或 insertSchema 推断)
497
- * @typeParam TInsert - 创建输入类型(从 insertSchema 推断)
498
- * @typeParam TUpdate - 更新输入类型(从 updateSchema 推断)
499
- */
500
567
  function createCrudRouter(config) {
501
- const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
568
+ const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
569
+ const { schema, updateSchema } = resolveSchemas(config);
502
570
  const resolved = resolveConfig(config);
503
571
  const softDelete = resolveSoftDelete(softDeleteOption);
504
572
  const m = middleware;
@@ -596,7 +664,7 @@ function createCrudRouter(config) {
596
664
  });
597
665
  return doGet();
598
666
  }),
599
- create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
667
+ create: resolved.procedureFactory("create").input(schema).mutation(async ({ ctx, input }) => {
600
668
  await withGuard(ctx, "create");
601
669
  const doCreate = async (inputData) => {
602
670
  const injectData = resolved.getInject(ctx, "create");
@@ -703,7 +771,7 @@ function createCrudRouter(config) {
703
771
  });
704
772
  return doUpdateMany(input.data);
705
773
  }),
706
- upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
774
+ upsert: resolved.procedureFactory("upsert").input(schema).mutation(async ({ ctx, input }) => {
707
775
  await withGuard(ctx, "upsert");
708
776
  const doUpsert = async (inputData) => {
709
777
  const inputId = inputData[idField];
@@ -739,6 +807,131 @@ function createCrudRouter(config) {
739
807
  next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
740
808
  });
741
809
  return doUpsert(input);
810
+ }),
811
+ export: resolved.procedureFactory("export").input(exportInputSchema).query(async ({ ctx, input }) => {
812
+ await withGuard(ctx, "export");
813
+ const doExport = async (exportInput$1) => {
814
+ const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
815
+ const validatedFilters = exportInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
816
+ const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
817
+ table,
818
+ filters: validatedFilters,
819
+ joinOperator: exportInput$1.joinOperator ?? "and"
820
+ }) : void 0);
821
+ let query = ctx.db.select().from(table).$dynamic();
822
+ if (where) query = query.where(where);
823
+ if (exportInput$1.sort?.length) {
824
+ const sortField = exportInput$1.sort[0];
825
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
826
+ const column = table[sortField.id];
827
+ if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
828
+ }
829
+ }
830
+ const data = await query.limit(effectiveLimit);
831
+ let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
832
+ if (where) countQuery = countQuery.where(where);
833
+ const total = (await countQuery)[0]?.count ?? 0;
834
+ return {
835
+ data,
836
+ total,
837
+ hasMore: total > data.length
838
+ };
839
+ };
840
+ const exportInput = input;
841
+ if (m.export) return m.export({
842
+ ctx,
843
+ input: exportInput,
844
+ next: async (modifiedInput) => doExport(modifiedInput ?? exportInput)
845
+ });
846
+ return doExport(exportInput);
847
+ }),
848
+ createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
849
+ await withGuard(ctx, "createMany");
850
+ const doCreateMany = async (items) => {
851
+ const injectData = resolved.getInject(ctx, "create");
852
+ const values = items.map((item) => ({
853
+ ...item,
854
+ ...injectData
855
+ }));
856
+ const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
857
+ return {
858
+ created,
859
+ count: created.length
860
+ };
861
+ };
862
+ if (m.createMany) return m.createMany({
863
+ ctx,
864
+ input,
865
+ next: async (modifiedInput) => doCreateMany(modifiedInput ?? input)
866
+ });
867
+ return doCreateMany(input);
868
+ }),
869
+ import: resolved.procedureFactory("import").input(importInputSchema).mutation(async ({ ctx, input }) => {
870
+ await withGuard(ctx, "import");
871
+ const doImport = async (importData$1) => {
872
+ const validRows = [];
873
+ const failed = [];
874
+ for (let i = 0; i < importData$1.rows.length; i++) {
875
+ const row = importData$1.rows[i];
876
+ const result = schema.safeParse(row);
877
+ if (result.success) validRows.push(result.data);
878
+ else {
879
+ const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
880
+ failed.push({
881
+ row: i,
882
+ errors
883
+ });
884
+ if (importData$1.onConflict === "error") return {
885
+ success: 0,
886
+ updated: 0,
887
+ skipped: 0,
888
+ failed
889
+ };
890
+ }
891
+ }
892
+ if (validRows.length === 0) return {
893
+ success: 0,
894
+ updated: 0,
895
+ skipped: 0,
896
+ failed
897
+ };
898
+ const injectData = resolved.getInject(ctx, "create");
899
+ const values = validRows.map((row) => ({
900
+ ...row,
901
+ ...injectData
902
+ }));
903
+ let insertedCount = 0;
904
+ let updatedCount = 0;
905
+ if (importData$1.onConflict === "upsert") {
906
+ const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where((0, drizzle_orm.inArray)(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
907
+ const updateFields = Object.keys(validRows[0]);
908
+ const setClause = { ...resolved.getInject(ctx, "update") };
909
+ for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
910
+ const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
911
+ target: getIdColumn(),
912
+ set: setClause
913
+ }).returning({ id: getIdColumn() });
914
+ updatedCount = Math.min(existingCount, results.length);
915
+ insertedCount = results.length - updatedCount;
916
+ } else {
917
+ insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
918
+ updatedCount = 0;
919
+ }
920
+ const skipped = validRows.length - insertedCount - updatedCount;
921
+ return {
922
+ success: insertedCount,
923
+ updated: updatedCount,
924
+ skipped,
925
+ failed
926
+ };
927
+ };
928
+ const importData = input;
929
+ if (m.import) return m.import({
930
+ ctx,
931
+ input: importData,
932
+ next: async (modifiedInput) => doImport(modifiedInput ?? importData)
933
+ });
934
+ return doImport(importData);
742
935
  })
743
936
  };
744
937
  const crudRouter = router(procedures);