@wordrhyme/auto-crud-server 0.9.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/dist/index.cjs +146 -1
- package/dist/index.d.cts +113 -2
- package/dist/index.d.ts +113 -2
- package/dist/index.js +146 -1
- package/package.json +7 -7
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.
|
|
4
|
+
"version": "0.10.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": "
|
|
43
|
-
"drizzle-zod": "
|
|
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",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"zod": "^4.1.13",
|
|
50
50
|
"@internal/eslint-config": "0.3.0",
|
|
51
51
|
"@internal/tsconfig": "0.1.0",
|
|
52
|
-
"@internal/prettier-config": "0.0.1",
|
|
53
52
|
"@internal/tsdown-config": "0.1.0",
|
|
54
|
-
"@internal/vitest-config": "0.1.0"
|
|
53
|
+
"@internal/vitest-config": "0.1.0",
|
|
54
|
+
"@internal/prettier-config": "0.0.1"
|
|
55
55
|
},
|
|
56
56
|
"prettier": "@internal/prettier-config",
|
|
57
57
|
"scripts": {
|