@wordrhyme/auto-crud-server 0.9.0 → 1.0.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/dist/index.cjs CHANGED
@@ -456,6 +456,9 @@ function isProcedureMap(config) {
456
456
  "deleteMany",
457
457
  "updateMany",
458
458
  "upsert",
459
+ "export",
460
+ "import",
461
+ "createMany",
459
462
  "default"
460
463
  ];
461
464
  return keys.some((k) => validKeys.includes(k));
@@ -507,6 +510,23 @@ const listInputSchema = zod.z.object({
507
510
  filters: zod.z.array(filterItemSchema).optional(),
508
511
  joinOperator: zod.z.enum(["and", "or"]).default("and")
509
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
+ });
510
530
  const DEFAULT_OMIT_FIELDS = [
511
531
  "id",
512
532
  "createdAt",
@@ -545,7 +565,7 @@ function validateColumn(columnId, allowedColumns) {
545
565
  return allowedColumns.includes(columnId);
546
566
  }
547
567
  function createCrudRouter(config) {
548
- const { table, 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;
549
569
  const { schema, updateSchema } = resolveSchemas(config);
550
570
  const resolved = resolveConfig(config);
551
571
  const softDelete = resolveSoftDelete(softDeleteOption);
@@ -787,6 +807,131 @@ function createCrudRouter(config) {
787
807
  next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
788
808
  });
789
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);
790
935
  })
791
936
  };
792
937
  const crudRouter = router(procedures);
package/dist/index.d.cts CHANGED
@@ -21,7 +21,7 @@ declare const router: _trpc_server2.TRPCRouterBuilder<{
21
21
  declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
22
22
  //#endregion
23
23
  //#region src/types/config.d.ts
24
- type CrudOperation = "list" | "get" | "create" | "update" | "delete" | "deleteMany" | "updateMany" | "upsert";
24
+ type CrudOperation = "list" | "get" | "create" | "update" | "delete" | "deleteMany" | "updateMany" | "upsert" | "export" | "import" | "createMany";
25
25
  type WriteOperation = "create" | "update";
26
26
  /**
27
27
  * 表字段键类型提取
@@ -75,6 +75,70 @@ interface ListResult<TSelect> {
75
75
  perPage: number;
76
76
  pageCount: number;
77
77
  }
78
+ /**
79
+ * 导出查询输入类型(复用 ListInput 的筛选/排序,无分页)
80
+ */
81
+ interface ExportInput {
82
+ sort?: Array<{
83
+ id: string;
84
+ desc: boolean;
85
+ }>;
86
+ filters?: Array<{
87
+ id: string;
88
+ value: string | string[];
89
+ variant: string;
90
+ operator: string;
91
+ filterId: string;
92
+ }>;
93
+ joinOperator?: "and" | "or";
94
+ /** 导出数量限制,会被 clamp 到 [1, maxExportSize] */
95
+ limit?: number;
96
+ }
97
+ /**
98
+ * 导出查询结果类型
99
+ */
100
+ interface ExportResult<TSelect> {
101
+ data: TSelect[];
102
+ total: number;
103
+ /** 是否还有更多数据未导出(total > data.length) */
104
+ hasMore: boolean;
105
+ }
106
+ /**
107
+ * 导入行失败详情
108
+ */
109
+ interface ImportFailedRow {
110
+ /** 行号(0-indexed) */
111
+ row: number;
112
+ /** 验证错误信息列表 */
113
+ errors: string[];
114
+ }
115
+ /**
116
+ * 导入结果类型
117
+ */
118
+ interface ImportResult {
119
+ /** 成功新建的行数 */
120
+ success: number;
121
+ /** 更新的行数(onConflict="upsert" 时) */
122
+ updated: number;
123
+ /** 跳过的行数(如重复 ID,onConflict="skip" 时) */
124
+ skipped: number;
125
+ /** 失败的行详情 */
126
+ failed: ImportFailedRow[];
127
+ }
128
+ /**
129
+ * 导入输入类型
130
+ */
131
+ interface ImportInput {
132
+ /** 待导入的数据行(客户端已解析为对象数组) */
133
+ rows: unknown[];
134
+ /**
135
+ * 冲突处理策略
136
+ * - "skip": 跳过冲突行(默认)
137
+ * - "upsert": 存在则更新,不存在则新建
138
+ * - "error": 冲突行计入失败
139
+ */
140
+ onConflict?: "skip" | "upsert" | "error";
141
+ }
78
142
  interface ListMiddlewareParams<TContext, TSelect> {
79
143
  ctx: TContext;
80
144
  input: ListInput;
@@ -126,6 +190,24 @@ interface UpsertMiddlewareParams<TContext, TSelect, TInsert> {
126
190
  isNew: boolean;
127
191
  }>;
128
192
  }
193
+ interface ExportMiddlewareParams<TContext, TSelect> {
194
+ ctx: TContext;
195
+ input: ExportInput;
196
+ next: (input?: ExportInput) => Promise<ExportResult<TSelect>>;
197
+ }
198
+ interface ImportMiddlewareParams<TContext> {
199
+ ctx: TContext;
200
+ input: ImportInput;
201
+ next: (input?: ImportInput) => Promise<ImportResult>;
202
+ }
203
+ interface CreateManyMiddlewareParams<TContext, TSelect, TInsert> {
204
+ ctx: TContext;
205
+ input: TInsert[];
206
+ next: (input?: TInsert[]) => Promise<{
207
+ created: TSelect[];
208
+ count: number;
209
+ }>;
210
+ }
129
211
  /**
130
212
  * 操作包装器 - 类似 tRPC middleware 的 next 模式
131
213
  *
@@ -195,6 +277,23 @@ interface CrudMiddleware<TContext = unknown, TSelect = unknown, TInsert = unknow
195
277
  data: TSelect;
196
278
  isNew: boolean;
197
279
  }>;
280
+ /**
281
+ * 导出包装器
282
+ * 可用于审计日志、限流、异步队列调度
283
+ */
284
+ export?: (params: ExportMiddlewareParams<TContext, TSelect>) => Promise<ExportResult<TSelect>>;
285
+ /**
286
+ * 导入包装器
287
+ * 可用于审计日志、限流、异步队列调度
288
+ */
289
+ import?: (params: ImportMiddlewareParams<TContext>) => Promise<ImportResult>;
290
+ /**
291
+ * 批量创建包装器
292
+ */
293
+ createMany?: (params: CreateManyMiddlewareParams<TContext, TSelect, TInsert>) => Promise<{
294
+ created: TSelect[];
295
+ count: number;
296
+ }>;
198
297
  }
199
298
  /**
200
299
  * Procedure 映射对象
@@ -209,6 +308,9 @@ interface ProcedureMap {
209
308
  deleteMany?: AnyProcedure;
210
309
  updateMany?: AnyProcedure;
211
310
  upsert?: AnyProcedure;
311
+ export?: AnyProcedure;
312
+ import?: AnyProcedure;
313
+ createMany?: AnyProcedure;
212
314
  /** 未指定操作时的默认 procedure */
213
315
  default?: AnyProcedure;
214
316
  }
@@ -362,6 +464,12 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
362
464
  sortableColumns?: string[];
363
465
  /** 批量操作的最大数量限制,默认 100 */
364
466
  maxBatchSize?: number;
467
+ /**
468
+ * 导出数量上限,默认 5000
469
+ * 超过此数量时 export 路由返回 hasMore=true
470
+ * 消费端可通过 middleware.export 接入异步队列处理大量数据
471
+ */
472
+ maxExportSize?: number;
365
473
  /**
366
474
  * 软删除配置
367
475
  * 开启后:
@@ -419,6 +527,9 @@ interface CrudProcedures<TSelect, TInsert, TUpdate> {
419
527
  deleteMany: AnyProcedure;
420
528
  updateMany: AnyProcedure;
421
529
  upsert: AnyProcedure;
530
+ export: AnyProcedure;
531
+ import: AnyProcedure;
532
+ createMany: AnyProcedure;
422
533
  }
423
534
  //#endregion
424
535
  //#region src/routers/_factory.d.ts
@@ -516,4 +627,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
516
627
  }, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
517
628
  type AppRouter = typeof appRouter;
518
629
  //#endregion
519
- export { type AnyProcedure, type AppRouter, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type GetMiddlewareParams, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
630
+ export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ declare const router: _trpc_server2.TRPCRouterBuilder<{
22
22
  declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
23
23
  //#endregion
24
24
  //#region src/types/config.d.ts
25
- type CrudOperation = "list" | "get" | "create" | "update" | "delete" | "deleteMany" | "updateMany" | "upsert";
25
+ type CrudOperation = "list" | "get" | "create" | "update" | "delete" | "deleteMany" | "updateMany" | "upsert" | "export" | "import" | "createMany";
26
26
  type WriteOperation = "create" | "update";
27
27
  /**
28
28
  * 表字段键类型提取
@@ -76,6 +76,70 @@ interface ListResult<TSelect> {
76
76
  perPage: number;
77
77
  pageCount: number;
78
78
  }
79
+ /**
80
+ * 导出查询输入类型(复用 ListInput 的筛选/排序,无分页)
81
+ */
82
+ interface ExportInput {
83
+ sort?: Array<{
84
+ id: string;
85
+ desc: boolean;
86
+ }>;
87
+ filters?: Array<{
88
+ id: string;
89
+ value: string | string[];
90
+ variant: string;
91
+ operator: string;
92
+ filterId: string;
93
+ }>;
94
+ joinOperator?: "and" | "or";
95
+ /** 导出数量限制,会被 clamp 到 [1, maxExportSize] */
96
+ limit?: number;
97
+ }
98
+ /**
99
+ * 导出查询结果类型
100
+ */
101
+ interface ExportResult<TSelect> {
102
+ data: TSelect[];
103
+ total: number;
104
+ /** 是否还有更多数据未导出(total > data.length) */
105
+ hasMore: boolean;
106
+ }
107
+ /**
108
+ * 导入行失败详情
109
+ */
110
+ interface ImportFailedRow {
111
+ /** 行号(0-indexed) */
112
+ row: number;
113
+ /** 验证错误信息列表 */
114
+ errors: string[];
115
+ }
116
+ /**
117
+ * 导入结果类型
118
+ */
119
+ interface ImportResult {
120
+ /** 成功新建的行数 */
121
+ success: number;
122
+ /** 更新的行数(onConflict="upsert" 时) */
123
+ updated: number;
124
+ /** 跳过的行数(如重复 ID,onConflict="skip" 时) */
125
+ skipped: number;
126
+ /** 失败的行详情 */
127
+ failed: ImportFailedRow[];
128
+ }
129
+ /**
130
+ * 导入输入类型
131
+ */
132
+ interface ImportInput {
133
+ /** 待导入的数据行(客户端已解析为对象数组) */
134
+ rows: unknown[];
135
+ /**
136
+ * 冲突处理策略
137
+ * - "skip": 跳过冲突行(默认)
138
+ * - "upsert": 存在则更新,不存在则新建
139
+ * - "error": 冲突行计入失败
140
+ */
141
+ onConflict?: "skip" | "upsert" | "error";
142
+ }
79
143
  interface ListMiddlewareParams<TContext, TSelect> {
80
144
  ctx: TContext;
81
145
  input: ListInput;
@@ -127,6 +191,24 @@ interface UpsertMiddlewareParams<TContext, TSelect, TInsert> {
127
191
  isNew: boolean;
128
192
  }>;
129
193
  }
194
+ interface ExportMiddlewareParams<TContext, TSelect> {
195
+ ctx: TContext;
196
+ input: ExportInput;
197
+ next: (input?: ExportInput) => Promise<ExportResult<TSelect>>;
198
+ }
199
+ interface ImportMiddlewareParams<TContext> {
200
+ ctx: TContext;
201
+ input: ImportInput;
202
+ next: (input?: ImportInput) => Promise<ImportResult>;
203
+ }
204
+ interface CreateManyMiddlewareParams<TContext, TSelect, TInsert> {
205
+ ctx: TContext;
206
+ input: TInsert[];
207
+ next: (input?: TInsert[]) => Promise<{
208
+ created: TSelect[];
209
+ count: number;
210
+ }>;
211
+ }
130
212
  /**
131
213
  * 操作包装器 - 类似 tRPC middleware 的 next 模式
132
214
  *
@@ -196,6 +278,23 @@ interface CrudMiddleware<TContext = unknown, TSelect = unknown, TInsert = unknow
196
278
  data: TSelect;
197
279
  isNew: boolean;
198
280
  }>;
281
+ /**
282
+ * 导出包装器
283
+ * 可用于审计日志、限流、异步队列调度
284
+ */
285
+ export?: (params: ExportMiddlewareParams<TContext, TSelect>) => Promise<ExportResult<TSelect>>;
286
+ /**
287
+ * 导入包装器
288
+ * 可用于审计日志、限流、异步队列调度
289
+ */
290
+ import?: (params: ImportMiddlewareParams<TContext>) => Promise<ImportResult>;
291
+ /**
292
+ * 批量创建包装器
293
+ */
294
+ createMany?: (params: CreateManyMiddlewareParams<TContext, TSelect, TInsert>) => Promise<{
295
+ created: TSelect[];
296
+ count: number;
297
+ }>;
199
298
  }
200
299
  /**
201
300
  * Procedure 映射对象
@@ -210,6 +309,9 @@ interface ProcedureMap {
210
309
  deleteMany?: AnyProcedure;
211
310
  updateMany?: AnyProcedure;
212
311
  upsert?: AnyProcedure;
312
+ export?: AnyProcedure;
313
+ import?: AnyProcedure;
314
+ createMany?: AnyProcedure;
213
315
  /** 未指定操作时的默认 procedure */
214
316
  default?: AnyProcedure;
215
317
  }
@@ -363,6 +465,12 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
363
465
  sortableColumns?: string[];
364
466
  /** 批量操作的最大数量限制,默认 100 */
365
467
  maxBatchSize?: number;
468
+ /**
469
+ * 导出数量上限,默认 5000
470
+ * 超过此数量时 export 路由返回 hasMore=true
471
+ * 消费端可通过 middleware.export 接入异步队列处理大量数据
472
+ */
473
+ maxExportSize?: number;
366
474
  /**
367
475
  * 软删除配置
368
476
  * 开启后:
@@ -420,6 +528,9 @@ interface CrudProcedures<TSelect, TInsert, TUpdate> {
420
528
  deleteMany: AnyProcedure;
421
529
  updateMany: AnyProcedure;
422
530
  upsert: AnyProcedure;
531
+ export: AnyProcedure;
532
+ import: AnyProcedure;
533
+ createMany: AnyProcedure;
423
534
  }
424
535
  //#endregion
425
536
  //#region src/routers/_factory.d.ts
@@ -517,4 +628,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
517
628
  }, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
518
629
  type AppRouter = typeof appRouter;
519
630
  //#endregion
520
- export { type AnyProcedure, type AppRouter, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type GetMiddlewareParams, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
631
+ export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
package/dist/index.js CHANGED
@@ -427,6 +427,9 @@ function isProcedureMap(config) {
427
427
  "deleteMany",
428
428
  "updateMany",
429
429
  "upsert",
430
+ "export",
431
+ "import",
432
+ "createMany",
430
433
  "default"
431
434
  ];
432
435
  return keys.some((k) => validKeys.includes(k));
@@ -478,6 +481,23 @@ const listInputSchema = z.object({
478
481
  filters: z.array(filterItemSchema).optional(),
479
482
  joinOperator: z.enum(["and", "or"]).default("and")
480
483
  });
484
+ const exportInputSchema = z.object({
485
+ sort: z.array(z.object({
486
+ id: z.string(),
487
+ desc: z.boolean()
488
+ })).optional(),
489
+ filters: z.array(filterItemSchema).optional(),
490
+ joinOperator: z.enum(["and", "or"]).default("and"),
491
+ limit: z.number().min(1).optional()
492
+ });
493
+ const importInputSchema = z.object({
494
+ rows: z.array(z.unknown()).min(1).max(1e3),
495
+ onConflict: z.enum([
496
+ "skip",
497
+ "upsert",
498
+ "error"
499
+ ]).default("skip")
500
+ });
481
501
  const DEFAULT_OMIT_FIELDS = [
482
502
  "id",
483
503
  "createdAt",
@@ -516,7 +536,7 @@ function validateColumn(columnId, allowedColumns) {
516
536
  return allowedColumns.includes(columnId);
517
537
  }
518
538
  function createCrudRouter(config) {
519
- const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
539
+ const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
520
540
  const { schema, updateSchema } = resolveSchemas(config);
521
541
  const resolved = resolveConfig(config);
522
542
  const softDelete = resolveSoftDelete(softDeleteOption);
@@ -758,6 +778,131 @@ function createCrudRouter(config) {
758
778
  next: async (modifiedInput) => doUpsert(modifiedInput ?? input)
759
779
  });
760
780
  return doUpsert(input);
781
+ }),
782
+ export: resolved.procedureFactory("export").input(exportInputSchema).query(async ({ ctx, input }) => {
783
+ await withGuard(ctx, "export");
784
+ const doExport = async (exportInput$1) => {
785
+ const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
786
+ const validatedFilters = exportInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
787
+ const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
788
+ table,
789
+ filters: validatedFilters,
790
+ joinOperator: exportInput$1.joinOperator ?? "and"
791
+ }) : void 0);
792
+ let query = ctx.db.select().from(table).$dynamic();
793
+ if (where) query = query.where(where);
794
+ if (exportInput$1.sort?.length) {
795
+ const sortField = exportInput$1.sort[0];
796
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
797
+ const column = table[sortField.id];
798
+ if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
799
+ }
800
+ }
801
+ const data = await query.limit(effectiveLimit);
802
+ let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
803
+ if (where) countQuery = countQuery.where(where);
804
+ const total = (await countQuery)[0]?.count ?? 0;
805
+ return {
806
+ data,
807
+ total,
808
+ hasMore: total > data.length
809
+ };
810
+ };
811
+ const exportInput = input;
812
+ if (m.export) return m.export({
813
+ ctx,
814
+ input: exportInput,
815
+ next: async (modifiedInput) => doExport(modifiedInput ?? exportInput)
816
+ });
817
+ return doExport(exportInput);
818
+ }),
819
+ createMany: resolved.procedureFactory("createMany").input(z.array(schema).min(1).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
820
+ await withGuard(ctx, "createMany");
821
+ const doCreateMany = async (items) => {
822
+ const injectData = resolved.getInject(ctx, "create");
823
+ const values = items.map((item) => ({
824
+ ...item,
825
+ ...injectData
826
+ }));
827
+ const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
828
+ return {
829
+ created,
830
+ count: created.length
831
+ };
832
+ };
833
+ if (m.createMany) return m.createMany({
834
+ ctx,
835
+ input,
836
+ next: async (modifiedInput) => doCreateMany(modifiedInput ?? input)
837
+ });
838
+ return doCreateMany(input);
839
+ }),
840
+ import: resolved.procedureFactory("import").input(importInputSchema).mutation(async ({ ctx, input }) => {
841
+ await withGuard(ctx, "import");
842
+ const doImport = async (importData$1) => {
843
+ const validRows = [];
844
+ const failed = [];
845
+ for (let i = 0; i < importData$1.rows.length; i++) {
846
+ const row = importData$1.rows[i];
847
+ const result = schema.safeParse(row);
848
+ if (result.success) validRows.push(result.data);
849
+ else {
850
+ const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
851
+ failed.push({
852
+ row: i,
853
+ errors
854
+ });
855
+ if (importData$1.onConflict === "error") return {
856
+ success: 0,
857
+ updated: 0,
858
+ skipped: 0,
859
+ failed
860
+ };
861
+ }
862
+ }
863
+ if (validRows.length === 0) return {
864
+ success: 0,
865
+ updated: 0,
866
+ skipped: 0,
867
+ failed
868
+ };
869
+ const injectData = resolved.getInject(ctx, "create");
870
+ const values = validRows.map((row) => ({
871
+ ...row,
872
+ ...injectData
873
+ }));
874
+ let insertedCount = 0;
875
+ let updatedCount = 0;
876
+ if (importData$1.onConflict === "upsert") {
877
+ const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where(inArray(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
878
+ const updateFields = Object.keys(validRows[0]);
879
+ const setClause = { ...resolved.getInject(ctx, "update") };
880
+ for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
881
+ const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
882
+ target: getIdColumn(),
883
+ set: setClause
884
+ }).returning({ id: getIdColumn() });
885
+ updatedCount = Math.min(existingCount, results.length);
886
+ insertedCount = results.length - updatedCount;
887
+ } else {
888
+ insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
889
+ updatedCount = 0;
890
+ }
891
+ const skipped = validRows.length - insertedCount - updatedCount;
892
+ return {
893
+ success: insertedCount,
894
+ updated: updatedCount,
895
+ skipped,
896
+ failed
897
+ };
898
+ };
899
+ const importData = input;
900
+ if (m.import) return m.import({
901
+ ctx,
902
+ input: importData,
903
+ next: async (modifiedInput) => doImport(modifiedInput ?? importData)
904
+ });
905
+ return doImport(importData);
761
906
  })
762
907
  };
763
908
  const crudRouter = router(procedures);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud-server",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "1.0.0",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -31,16 +31,16 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@trpc/server": "^11.0.0",
34
- "drizzle-orm": "^0.40.0",
35
- "drizzle-zod": "^0.8.0",
34
+ "drizzle-orm": "^0.40.0 || ^1.0.0-beta",
35
+ "drizzle-zod": "^0.8.0 || ^1.0.0-beta",
36
36
  "superjson": "^2.0.0",
37
37
  "zod": "^3.0.0 || ^4.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@trpc/server": "^11.0.0",
41
41
  "@types/node": "^22.19.3",
42
- "drizzle-orm": "^0.45.1",
43
- "drizzle-zod": "^0.8.3",
42
+ "drizzle-orm": "1.0.0-beta.15-859cf75",
43
+ "drizzle-zod": "1.0.0-beta.14-a36c63d",
44
44
  "eslint": "^9.39.2",
45
45
  "superjson": "^2.2.6",
46
46
  "tsdown": "^0.15.12",
@@ -48,8 +48,8 @@
48
48
  "vitest": "^4.0.18",
49
49
  "zod": "^4.1.13",
50
50
  "@internal/eslint-config": "0.3.0",
51
- "@internal/tsconfig": "0.1.0",
52
51
  "@internal/prettier-config": "0.0.1",
52
+ "@internal/tsconfig": "0.1.0",
53
53
  "@internal/tsdown-config": "0.1.0",
54
54
  "@internal/vitest-config": "0.1.0"
55
55
  },