@wordrhyme/auto-crud-server 0.6.1 → 0.7.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 +80 -44
- package/dist/index.d.cts +126 -88
- package/dist/index.d.ts +126 -88
- package/dist/index.js +81 -45
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -417,38 +417,53 @@ function resolveSoftDelete(option) {
|
|
|
417
417
|
getValue: config.value ?? (() => /* @__PURE__ */ new Date())
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
420
|
+
/**
|
|
421
|
+
* 判断 procedure 配置的类型
|
|
422
|
+
*/
|
|
423
|
+
function isProcedureMap(config) {
|
|
424
|
+
if (!config) return false;
|
|
425
|
+
if (typeof config === "function") return false;
|
|
426
|
+
if (typeof config === "object" && config !== null) {
|
|
427
|
+
const keys = Object.keys(config);
|
|
428
|
+
const validKeys = [
|
|
429
|
+
"list",
|
|
430
|
+
"get",
|
|
431
|
+
"create",
|
|
432
|
+
"update",
|
|
433
|
+
"delete",
|
|
434
|
+
"deleteMany",
|
|
435
|
+
"updateMany",
|
|
436
|
+
"upsert",
|
|
437
|
+
"default"
|
|
438
|
+
];
|
|
439
|
+
return keys.some((k) => validKeys.includes(k));
|
|
431
440
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
function isProcedureFactory(config) {
|
|
444
|
+
return typeof config === "function";
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 解析 procedure 配置为工厂函数
|
|
448
|
+
*/
|
|
449
|
+
function resolveProcedureFactory(config) {
|
|
450
|
+
if (!config) return () => publicProcedure;
|
|
451
|
+
if (isProcedureFactory(config)) return config;
|
|
452
|
+
if (isProcedureMap(config)) {
|
|
453
|
+
const map = config;
|
|
454
|
+
const defaultProc = map.default ?? publicProcedure;
|
|
455
|
+
return (op) => map[op] ?? defaultProc;
|
|
442
456
|
}
|
|
443
|
-
|
|
444
|
-
|
|
457
|
+
return () => config;
|
|
458
|
+
}
|
|
459
|
+
function resolveConfig(config) {
|
|
445
460
|
return {
|
|
446
|
-
procedureFactory: ()
|
|
447
|
-
guard:
|
|
448
|
-
getScope: (ctx, tbl, op) =>
|
|
449
|
-
getInject: (ctx, op) =>
|
|
461
|
+
procedureFactory: resolveProcedureFactory(config.procedure),
|
|
462
|
+
guard: config.guard,
|
|
463
|
+
getScope: (ctx, tbl, op) => config.scope?.(ctx, tbl, op),
|
|
464
|
+
getInject: (ctx, op) => config.inject?.(ctx, op) ?? {},
|
|
450
465
|
checkAuthorize: async (ctx, resource, op) => {
|
|
451
|
-
if (
|
|
466
|
+
if (config.authorize) return config.authorize(ctx, resource, op);
|
|
452
467
|
return true;
|
|
453
468
|
}
|
|
454
469
|
};
|
|
@@ -493,7 +508,7 @@ function createCrudRouter(config) {
|
|
|
493
508
|
const conditions = [];
|
|
494
509
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
495
510
|
if (scopeCondition) conditions.push(scopeCondition);
|
|
496
|
-
if (softDelete
|
|
511
|
+
if (softDelete) {
|
|
497
512
|
const col = getSoftDeleteColumn();
|
|
498
513
|
if (col) conditions.push((0, drizzle_orm.isNull)(col));
|
|
499
514
|
}
|
|
@@ -507,15 +522,24 @@ function createCrudRouter(config) {
|
|
|
507
522
|
if (operation === "upsert") {
|
|
508
523
|
const canCreate = await resolved.guard(ctx, "create");
|
|
509
524
|
const canUpdate = await resolved.guard(ctx, "update");
|
|
510
|
-
if (!canCreate || !canUpdate) throw new
|
|
525
|
+
if (!canCreate || !canUpdate) throw new __trpc_server.TRPCError({
|
|
526
|
+
code: "FORBIDDEN",
|
|
527
|
+
message: "Forbidden: upsert requires both create and update permissions"
|
|
528
|
+
});
|
|
511
529
|
return;
|
|
512
530
|
}
|
|
513
|
-
if (!await resolved.guard(ctx, operation)) throw new
|
|
531
|
+
if (!await resolved.guard(ctx, operation)) throw new __trpc_server.TRPCError({
|
|
532
|
+
code: "FORBIDDEN",
|
|
533
|
+
message: `Forbidden: ${operation} not allowed`
|
|
534
|
+
});
|
|
514
535
|
}
|
|
515
536
|
};
|
|
516
537
|
const withAuthorize = async (ctx, resource, operation) => {
|
|
517
538
|
if (!resource) return;
|
|
518
|
-
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new
|
|
539
|
+
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new __trpc_server.TRPCError({
|
|
540
|
+
code: "FORBIDDEN",
|
|
541
|
+
message: `Forbidden: Cannot ${operation} this resource`
|
|
542
|
+
});
|
|
519
543
|
};
|
|
520
544
|
const procedures = {
|
|
521
545
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
@@ -598,7 +622,10 @@ function createCrudRouter(config) {
|
|
|
598
622
|
const where = buildWhere(ctx, "update", (0, drizzle_orm.eq)(getIdColumn(), input.id));
|
|
599
623
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
600
624
|
await withAuthorize(ctx, existing, "update");
|
|
601
|
-
if (!existing) throw new
|
|
625
|
+
if (!existing) throw new __trpc_server.TRPCError({
|
|
626
|
+
code: "NOT_FOUND",
|
|
627
|
+
message: "Resource not found or access denied"
|
|
628
|
+
});
|
|
602
629
|
const doUpdate = async (updateData) => {
|
|
603
630
|
const injectData = resolved.getInject(ctx, "update");
|
|
604
631
|
const data = {
|
|
@@ -622,7 +649,10 @@ function createCrudRouter(config) {
|
|
|
622
649
|
const where = buildWhere(ctx, "delete", (0, drizzle_orm.eq)(getIdColumn(), input));
|
|
623
650
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
624
651
|
await withAuthorize(ctx, existing, "delete");
|
|
625
|
-
if (!existing) throw new
|
|
652
|
+
if (!existing) throw new __trpc_server.TRPCError({
|
|
653
|
+
code: "NOT_FOUND",
|
|
654
|
+
message: "Resource not found or access denied"
|
|
655
|
+
});
|
|
626
656
|
const doDelete = async () => {
|
|
627
657
|
let deleted;
|
|
628
658
|
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
@@ -641,8 +671,8 @@ function createCrudRouter(config) {
|
|
|
641
671
|
await withGuard(ctx, "deleteMany");
|
|
642
672
|
const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
|
|
643
673
|
const doDeleteMany = async () => {
|
|
644
|
-
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
645
|
-
else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
674
|
+
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
|
|
675
|
+
else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
|
|
646
676
|
};
|
|
647
677
|
if (m.deleteMany) return m.deleteMany({
|
|
648
678
|
ctx,
|
|
@@ -663,7 +693,7 @@ function createCrudRouter(config) {
|
|
|
663
693
|
...updateData,
|
|
664
694
|
...injectData
|
|
665
695
|
};
|
|
666
|
-
return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
696
|
+
return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
|
|
667
697
|
};
|
|
668
698
|
if (m.updateMany) return m.updateMany({
|
|
669
699
|
ctx,
|
|
@@ -676,21 +706,27 @@ function createCrudRouter(config) {
|
|
|
676
706
|
upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
677
707
|
await withGuard(ctx, "upsert");
|
|
678
708
|
const doUpsert = async (inputData) => {
|
|
679
|
-
const injectData = resolved.getInject(ctx, "create");
|
|
680
|
-
const data = {
|
|
681
|
-
...inputData,
|
|
682
|
-
...injectData
|
|
683
|
-
};
|
|
684
709
|
const inputId = inputData[idField];
|
|
685
710
|
let isNew = true;
|
|
711
|
+
let existing = null;
|
|
686
712
|
if (inputId) {
|
|
687
713
|
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
|
|
688
|
-
const [
|
|
714
|
+
const [found] = await ctx.db.select().from(table).where(where);
|
|
715
|
+
existing = found;
|
|
689
716
|
isNew = !existing;
|
|
717
|
+
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
690
718
|
}
|
|
719
|
+
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
720
|
+
const data = {
|
|
721
|
+
...inputData,
|
|
722
|
+
...injectData
|
|
723
|
+
};
|
|
691
724
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
692
725
|
target: getIdColumn(),
|
|
693
|
-
set:
|
|
726
|
+
set: {
|
|
727
|
+
...inputData,
|
|
728
|
+
...resolved.getInject(ctx, "update")
|
|
729
|
+
}
|
|
694
730
|
}).returning();
|
|
695
731
|
return {
|
|
696
732
|
data: result,
|
package/dist/index.d.cts
CHANGED
|
@@ -191,15 +191,137 @@ interface CrudMiddleware<TContext = unknown, TSelect = unknown, TInsert = unknow
|
|
|
191
191
|
isNew: boolean;
|
|
192
192
|
}>;
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Procedure 映射对象
|
|
196
|
+
* 按操作指定不同的 procedure,支持 default 回退
|
|
197
|
+
*/
|
|
198
|
+
interface ProcedureMap {
|
|
199
|
+
list?: AnyProcedure;
|
|
200
|
+
get?: AnyProcedure;
|
|
201
|
+
create?: AnyProcedure;
|
|
202
|
+
update?: AnyProcedure;
|
|
203
|
+
delete?: AnyProcedure;
|
|
204
|
+
deleteMany?: AnyProcedure;
|
|
205
|
+
updateMany?: AnyProcedure;
|
|
206
|
+
upsert?: AnyProcedure;
|
|
207
|
+
/** 未指定操作时的默认 procedure */
|
|
208
|
+
default?: AnyProcedure;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Procedure 工厂函数
|
|
212
|
+
* 根据操作类型动态返回 procedure
|
|
213
|
+
*/
|
|
214
|
+
type ProcedureFactory = (operation: CrudOperation) => AnyProcedure;
|
|
215
|
+
/**
|
|
216
|
+
* 统一的 Procedure 配置
|
|
217
|
+
* 支持三种形式:
|
|
218
|
+
* 1. 单一 procedure - 所有操作共用
|
|
219
|
+
* 2. 对象映射 - 按操作指定
|
|
220
|
+
* 3. 工厂函数 - 动态生成
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* // 形式 1:单一 procedure
|
|
224
|
+
* procedure: protectedProcedure
|
|
225
|
+
*
|
|
226
|
+
* // 形式 2:对象映射(推荐常用场景)
|
|
227
|
+
* procedure: {
|
|
228
|
+
* list: publicProcedure,
|
|
229
|
+
* get: publicProcedure,
|
|
230
|
+
* create: editorProcedure,
|
|
231
|
+
* delete: adminProcedure,
|
|
232
|
+
* default: protectedProcedure,
|
|
233
|
+
* }
|
|
234
|
+
*
|
|
235
|
+
* // 形式 3:工厂函数(适合复杂逻辑)
|
|
236
|
+
* procedure: (op) => {
|
|
237
|
+
* if (['list', 'get'].includes(op)) return publicProcedure;
|
|
238
|
+
* return protectedProcedure.meta({ permission: { action: op, subject: 'Task' } });
|
|
239
|
+
* }
|
|
240
|
+
*/
|
|
241
|
+
type ProcedureConfig = AnyProcedure | ProcedureMap | ProcedureFactory;
|
|
242
|
+
/**
|
|
243
|
+
* CRUD Router 配置
|
|
244
|
+
*
|
|
245
|
+
* v2.0 统一 API 设计:
|
|
246
|
+
* - 所有配置项可自由组合,无互斥模式
|
|
247
|
+
* - procedure 支持多种形式(单一/映射/工厂)
|
|
248
|
+
* - guard/scope/authorize 可与任意 procedure 形式组合
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* // 简单场景:公开读,保护写
|
|
252
|
+
* createCrudRouter({
|
|
253
|
+
* table: posts,
|
|
254
|
+
* insertSchema,
|
|
255
|
+
* updateSchema,
|
|
256
|
+
* procedure: {
|
|
257
|
+
* list: publicProcedure,
|
|
258
|
+
* get: publicProcedure,
|
|
259
|
+
* default: protectedProcedure,
|
|
260
|
+
* },
|
|
261
|
+
* })
|
|
262
|
+
*
|
|
263
|
+
* // 复杂场景:自定义 procedure + 行级安全
|
|
264
|
+
* createCrudRouter({
|
|
265
|
+
* table: posts,
|
|
266
|
+
* insertSchema,
|
|
267
|
+
* updateSchema,
|
|
268
|
+
* procedure: adminProcedure,
|
|
269
|
+
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId),
|
|
270
|
+
* authorize: (ctx, resource) => resource.ownerId === ctx.user.id,
|
|
271
|
+
* })
|
|
272
|
+
*/
|
|
273
|
+
interface CrudRouterConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> {
|
|
195
274
|
/** Drizzle 表定义 */
|
|
196
275
|
table: PgTable;
|
|
197
|
-
/** 查询返回 Schema */
|
|
198
|
-
selectSchema?: z.ZodType<TSelect>;
|
|
199
276
|
/** 创建输入 Schema */
|
|
200
277
|
insertSchema: z.ZodType<TInsert>;
|
|
201
278
|
/** 更新输入 Schema */
|
|
202
279
|
updateSchema: z.ZodType<TUpdate>;
|
|
280
|
+
/**
|
|
281
|
+
* Procedure 配置(统一字段)
|
|
282
|
+
*
|
|
283
|
+
* 支持三种形式:
|
|
284
|
+
* 1. 单一 procedure - 所有操作共用
|
|
285
|
+
* 2. 对象映射 - 按操作指定(推荐)
|
|
286
|
+
* 3. 工厂函数 - 动态生成
|
|
287
|
+
*
|
|
288
|
+
* 默认: publicProcedure
|
|
289
|
+
*/
|
|
290
|
+
procedure?: ProcedureConfig;
|
|
291
|
+
/**
|
|
292
|
+
* 操作级守卫(RBAC)
|
|
293
|
+
* 在 scope 之前执行,快速拒绝无权限请求
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* guard: (ctx, op) => op === 'delete' ? ctx.user.role === 'admin' : true
|
|
297
|
+
*/
|
|
298
|
+
guard?: (ctx: TContext, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
299
|
+
/**
|
|
300
|
+
* 行级过滤(RLS)
|
|
301
|
+
* 返回 SQL 条件,自动注入到 WHERE 子句
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId)
|
|
305
|
+
*/
|
|
306
|
+
scope?: (ctx: TContext, table: PgTable, operation: CrudOperation) => SQL | undefined;
|
|
307
|
+
/**
|
|
308
|
+
* 强制注入字段(写入时)
|
|
309
|
+
* 返回的字段会强制覆盖用户输入(防止伪造)
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* inject: (ctx) => ({ tenantId: ctx.user.tenantId, createdBy: ctx.user.id })
|
|
313
|
+
*/
|
|
314
|
+
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
315
|
+
/**
|
|
316
|
+
* 资源级检查(ABAC)
|
|
317
|
+
* 在 get/update/delete 时,预取资源后调用
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* authorize: (ctx, resource, op) => resource.ownerId === ctx.user.id
|
|
321
|
+
*/
|
|
322
|
+
authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
323
|
+
/** 查询返回 Schema */
|
|
324
|
+
selectSchema?: z.ZodType<TSelect>;
|
|
203
325
|
/** ID 字段名,默认 "id" */
|
|
204
326
|
idField?: string;
|
|
205
327
|
/** 可过滤的列白名单 */
|
|
@@ -256,90 +378,6 @@ interface CrudRouterConfigBase<TContext = unknown, TSelect = unknown, TInsert =
|
|
|
256
378
|
*/
|
|
257
379
|
middleware?: CrudMiddleware<TContext, TSelect, TInsert, TUpdate>;
|
|
258
380
|
}
|
|
259
|
-
interface DeclarativeConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
260
|
-
mode?: "declarative";
|
|
261
|
-
/**
|
|
262
|
-
* 基础 procedure(认证)
|
|
263
|
-
* 所有操作共用,除非被 guard 拦截
|
|
264
|
-
*/
|
|
265
|
-
procedure?: AnyProcedure;
|
|
266
|
-
/**
|
|
267
|
-
* 操作级守卫(RBAC)
|
|
268
|
-
* 在 scope 之前执行,快速拒绝无权限请求
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* guard: (ctx, op) => op === 'delete' ? ctx.user.role === 'admin' : true
|
|
272
|
-
*/
|
|
273
|
-
guard?: (ctx: TContext, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
274
|
-
/**
|
|
275
|
-
* 行级过滤(RLS)
|
|
276
|
-
* 返回 SQL 条件,自动注入到 WHERE 子句
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId)
|
|
280
|
-
*/
|
|
281
|
-
scope?: (ctx: TContext, table: PgTable, operation: CrudOperation) => SQL | undefined;
|
|
282
|
-
/**
|
|
283
|
-
* 强制注入字段(写入时)
|
|
284
|
-
* 返回的字段会强制覆盖用户输入(防止伪造)
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* inject: (ctx) => ({ tenantId: ctx.user.tenantId, createdBy: ctx.user.id })
|
|
288
|
-
*/
|
|
289
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
290
|
-
/**
|
|
291
|
-
* 资源级检查(ABAC)
|
|
292
|
-
* 在 get/update/delete 时,预取资源后调用
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* authorize: (ctx, resource, op) => resource.ownerId === ctx.user.id
|
|
296
|
-
*/
|
|
297
|
-
authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
298
|
-
}
|
|
299
|
-
interface FactoryConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
300
|
-
mode: "factory";
|
|
301
|
-
/**
|
|
302
|
-
* Procedure 工厂函数
|
|
303
|
-
* 根据操作类型返回对应的 procedure
|
|
304
|
-
*
|
|
305
|
-
* @example
|
|
306
|
-
* procedureFactory: (op) => {
|
|
307
|
-
* if (['list', 'get'].includes(op)) return publicProcedure;
|
|
308
|
-
* return protectedProcedure.meta({ permission: { action: op, subject: 'Task' } });
|
|
309
|
-
* }
|
|
310
|
-
*/
|
|
311
|
-
procedureFactory: (operation: CrudOperation) => AnyProcedure;
|
|
312
|
-
/**
|
|
313
|
-
* 强制注入字段(可选)
|
|
314
|
-
* 如果 DB 层已处理则不需要
|
|
315
|
-
*/
|
|
316
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
317
|
-
}
|
|
318
|
-
interface ProceduresConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
319
|
-
mode: "procedures";
|
|
320
|
-
/**
|
|
321
|
-
* 按操作指定不同的 procedure
|
|
322
|
-
*
|
|
323
|
-
* @example
|
|
324
|
-
* procedures: {
|
|
325
|
-
* list: publicProcedure,
|
|
326
|
-
* get: publicProcedure,
|
|
327
|
-
* create: editorProcedure,
|
|
328
|
-
* update: editorProcedure,
|
|
329
|
-
* delete: adminProcedure,
|
|
330
|
-
* }
|
|
331
|
-
*/
|
|
332
|
-
procedures: Partial<Record<CrudOperation, AnyProcedure>>;
|
|
333
|
-
/**
|
|
334
|
-
* 默认 procedure(未指定操作时使用)
|
|
335
|
-
*/
|
|
336
|
-
defaultProcedure?: AnyProcedure;
|
|
337
|
-
/**
|
|
338
|
-
* 强制注入字段(可选)
|
|
339
|
-
*/
|
|
340
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
341
|
-
}
|
|
342
|
-
type CrudRouterConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> = DeclarativeConfig<TContext, TSelect, TInsert, TUpdate> | FactoryConfig<TContext, TSelect, TInsert, TUpdate> | ProceduresConfig<TContext, TSelect, TInsert, TUpdate>;
|
|
343
381
|
interface CrudProcedures<TSelect, TInsert, TUpdate> {
|
|
344
382
|
list: AnyProcedure;
|
|
345
383
|
get: AnyProcedure;
|
|
@@ -430,4 +468,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
430
468
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
431
469
|
type AppRouter = typeof appRouter;
|
|
432
470
|
//#endregion
|
|
433
|
-
export { type AnyProcedure, type AppRouter, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type
|
|
471
|
+
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -192,15 +192,137 @@ interface CrudMiddleware<TContext = unknown, TSelect = unknown, TInsert = unknow
|
|
|
192
192
|
isNew: boolean;
|
|
193
193
|
}>;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Procedure 映射对象
|
|
197
|
+
* 按操作指定不同的 procedure,支持 default 回退
|
|
198
|
+
*/
|
|
199
|
+
interface ProcedureMap {
|
|
200
|
+
list?: AnyProcedure;
|
|
201
|
+
get?: AnyProcedure;
|
|
202
|
+
create?: AnyProcedure;
|
|
203
|
+
update?: AnyProcedure;
|
|
204
|
+
delete?: AnyProcedure;
|
|
205
|
+
deleteMany?: AnyProcedure;
|
|
206
|
+
updateMany?: AnyProcedure;
|
|
207
|
+
upsert?: AnyProcedure;
|
|
208
|
+
/** 未指定操作时的默认 procedure */
|
|
209
|
+
default?: AnyProcedure;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Procedure 工厂函数
|
|
213
|
+
* 根据操作类型动态返回 procedure
|
|
214
|
+
*/
|
|
215
|
+
type ProcedureFactory = (operation: CrudOperation) => AnyProcedure;
|
|
216
|
+
/**
|
|
217
|
+
* 统一的 Procedure 配置
|
|
218
|
+
* 支持三种形式:
|
|
219
|
+
* 1. 单一 procedure - 所有操作共用
|
|
220
|
+
* 2. 对象映射 - 按操作指定
|
|
221
|
+
* 3. 工厂函数 - 动态生成
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // 形式 1:单一 procedure
|
|
225
|
+
* procedure: protectedProcedure
|
|
226
|
+
*
|
|
227
|
+
* // 形式 2:对象映射(推荐常用场景)
|
|
228
|
+
* procedure: {
|
|
229
|
+
* list: publicProcedure,
|
|
230
|
+
* get: publicProcedure,
|
|
231
|
+
* create: editorProcedure,
|
|
232
|
+
* delete: adminProcedure,
|
|
233
|
+
* default: protectedProcedure,
|
|
234
|
+
* }
|
|
235
|
+
*
|
|
236
|
+
* // 形式 3:工厂函数(适合复杂逻辑)
|
|
237
|
+
* procedure: (op) => {
|
|
238
|
+
* if (['list', 'get'].includes(op)) return publicProcedure;
|
|
239
|
+
* return protectedProcedure.meta({ permission: { action: op, subject: 'Task' } });
|
|
240
|
+
* }
|
|
241
|
+
*/
|
|
242
|
+
type ProcedureConfig = AnyProcedure | ProcedureMap | ProcedureFactory;
|
|
243
|
+
/**
|
|
244
|
+
* CRUD Router 配置
|
|
245
|
+
*
|
|
246
|
+
* v2.0 统一 API 设计:
|
|
247
|
+
* - 所有配置项可自由组合,无互斥模式
|
|
248
|
+
* - procedure 支持多种形式(单一/映射/工厂)
|
|
249
|
+
* - guard/scope/authorize 可与任意 procedure 形式组合
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // 简单场景:公开读,保护写
|
|
253
|
+
* createCrudRouter({
|
|
254
|
+
* table: posts,
|
|
255
|
+
* insertSchema,
|
|
256
|
+
* updateSchema,
|
|
257
|
+
* procedure: {
|
|
258
|
+
* list: publicProcedure,
|
|
259
|
+
* get: publicProcedure,
|
|
260
|
+
* default: protectedProcedure,
|
|
261
|
+
* },
|
|
262
|
+
* })
|
|
263
|
+
*
|
|
264
|
+
* // 复杂场景:自定义 procedure + 行级安全
|
|
265
|
+
* createCrudRouter({
|
|
266
|
+
* table: posts,
|
|
267
|
+
* insertSchema,
|
|
268
|
+
* updateSchema,
|
|
269
|
+
* procedure: adminProcedure,
|
|
270
|
+
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId),
|
|
271
|
+
* authorize: (ctx, resource) => resource.ownerId === ctx.user.id,
|
|
272
|
+
* })
|
|
273
|
+
*/
|
|
274
|
+
interface CrudRouterConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> {
|
|
196
275
|
/** Drizzle 表定义 */
|
|
197
276
|
table: PgTable;
|
|
198
|
-
/** 查询返回 Schema */
|
|
199
|
-
selectSchema?: z.ZodType<TSelect>;
|
|
200
277
|
/** 创建输入 Schema */
|
|
201
278
|
insertSchema: z.ZodType<TInsert>;
|
|
202
279
|
/** 更新输入 Schema */
|
|
203
280
|
updateSchema: z.ZodType<TUpdate>;
|
|
281
|
+
/**
|
|
282
|
+
* Procedure 配置(统一字段)
|
|
283
|
+
*
|
|
284
|
+
* 支持三种形式:
|
|
285
|
+
* 1. 单一 procedure - 所有操作共用
|
|
286
|
+
* 2. 对象映射 - 按操作指定(推荐)
|
|
287
|
+
* 3. 工厂函数 - 动态生成
|
|
288
|
+
*
|
|
289
|
+
* 默认: publicProcedure
|
|
290
|
+
*/
|
|
291
|
+
procedure?: ProcedureConfig;
|
|
292
|
+
/**
|
|
293
|
+
* 操作级守卫(RBAC)
|
|
294
|
+
* 在 scope 之前执行,快速拒绝无权限请求
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* guard: (ctx, op) => op === 'delete' ? ctx.user.role === 'admin' : true
|
|
298
|
+
*/
|
|
299
|
+
guard?: (ctx: TContext, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
300
|
+
/**
|
|
301
|
+
* 行级过滤(RLS)
|
|
302
|
+
* 返回 SQL 条件,自动注入到 WHERE 子句
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId)
|
|
306
|
+
*/
|
|
307
|
+
scope?: (ctx: TContext, table: PgTable, operation: CrudOperation) => SQL | undefined;
|
|
308
|
+
/**
|
|
309
|
+
* 强制注入字段(写入时)
|
|
310
|
+
* 返回的字段会强制覆盖用户输入(防止伪造)
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* inject: (ctx) => ({ tenantId: ctx.user.tenantId, createdBy: ctx.user.id })
|
|
314
|
+
*/
|
|
315
|
+
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
316
|
+
/**
|
|
317
|
+
* 资源级检查(ABAC)
|
|
318
|
+
* 在 get/update/delete 时,预取资源后调用
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* authorize: (ctx, resource, op) => resource.ownerId === ctx.user.id
|
|
322
|
+
*/
|
|
323
|
+
authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
324
|
+
/** 查询返回 Schema */
|
|
325
|
+
selectSchema?: z.ZodType<TSelect>;
|
|
204
326
|
/** ID 字段名,默认 "id" */
|
|
205
327
|
idField?: string;
|
|
206
328
|
/** 可过滤的列白名单 */
|
|
@@ -257,90 +379,6 @@ interface CrudRouterConfigBase<TContext = unknown, TSelect = unknown, TInsert =
|
|
|
257
379
|
*/
|
|
258
380
|
middleware?: CrudMiddleware<TContext, TSelect, TInsert, TUpdate>;
|
|
259
381
|
}
|
|
260
|
-
interface DeclarativeConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
261
|
-
mode?: "declarative";
|
|
262
|
-
/**
|
|
263
|
-
* 基础 procedure(认证)
|
|
264
|
-
* 所有操作共用,除非被 guard 拦截
|
|
265
|
-
*/
|
|
266
|
-
procedure?: AnyProcedure;
|
|
267
|
-
/**
|
|
268
|
-
* 操作级守卫(RBAC)
|
|
269
|
-
* 在 scope 之前执行,快速拒绝无权限请求
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* guard: (ctx, op) => op === 'delete' ? ctx.user.role === 'admin' : true
|
|
273
|
-
*/
|
|
274
|
-
guard?: (ctx: TContext, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
275
|
-
/**
|
|
276
|
-
* 行级过滤(RLS)
|
|
277
|
-
* 返回 SQL 条件,自动注入到 WHERE 子句
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* scope: (ctx, table) => eq(table.tenantId, ctx.user.tenantId)
|
|
281
|
-
*/
|
|
282
|
-
scope?: (ctx: TContext, table: PgTable, operation: CrudOperation) => SQL | undefined;
|
|
283
|
-
/**
|
|
284
|
-
* 强制注入字段(写入时)
|
|
285
|
-
* 返回的字段会强制覆盖用户输入(防止伪造)
|
|
286
|
-
*
|
|
287
|
-
* @example
|
|
288
|
-
* inject: (ctx) => ({ tenantId: ctx.user.tenantId, createdBy: ctx.user.id })
|
|
289
|
-
*/
|
|
290
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
291
|
-
/**
|
|
292
|
-
* 资源级检查(ABAC)
|
|
293
|
-
* 在 get/update/delete 时,预取资源后调用
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* authorize: (ctx, resource, op) => resource.ownerId === ctx.user.id
|
|
297
|
-
*/
|
|
298
|
-
authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
|
|
299
|
-
}
|
|
300
|
-
interface FactoryConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
301
|
-
mode: "factory";
|
|
302
|
-
/**
|
|
303
|
-
* Procedure 工厂函数
|
|
304
|
-
* 根据操作类型返回对应的 procedure
|
|
305
|
-
*
|
|
306
|
-
* @example
|
|
307
|
-
* procedureFactory: (op) => {
|
|
308
|
-
* if (['list', 'get'].includes(op)) return publicProcedure;
|
|
309
|
-
* return protectedProcedure.meta({ permission: { action: op, subject: 'Task' } });
|
|
310
|
-
* }
|
|
311
|
-
*/
|
|
312
|
-
procedureFactory: (operation: CrudOperation) => AnyProcedure;
|
|
313
|
-
/**
|
|
314
|
-
* 强制注入字段(可选)
|
|
315
|
-
* 如果 DB 层已处理则不需要
|
|
316
|
-
*/
|
|
317
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
318
|
-
}
|
|
319
|
-
interface ProceduresConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> extends CrudRouterConfigBase<TContext, TSelect, TInsert, TUpdate> {
|
|
320
|
-
mode: "procedures";
|
|
321
|
-
/**
|
|
322
|
-
* 按操作指定不同的 procedure
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
* procedures: {
|
|
326
|
-
* list: publicProcedure,
|
|
327
|
-
* get: publicProcedure,
|
|
328
|
-
* create: editorProcedure,
|
|
329
|
-
* update: editorProcedure,
|
|
330
|
-
* delete: adminProcedure,
|
|
331
|
-
* }
|
|
332
|
-
*/
|
|
333
|
-
procedures: Partial<Record<CrudOperation, AnyProcedure>>;
|
|
334
|
-
/**
|
|
335
|
-
* 默认 procedure(未指定操作时使用)
|
|
336
|
-
*/
|
|
337
|
-
defaultProcedure?: AnyProcedure;
|
|
338
|
-
/**
|
|
339
|
-
* 强制注入字段(可选)
|
|
340
|
-
*/
|
|
341
|
-
inject?: (ctx: TContext, operation: WriteOperation) => Record<string, unknown>;
|
|
342
|
-
}
|
|
343
|
-
type CrudRouterConfig<TContext = unknown, TSelect = unknown, TInsert = unknown, TUpdate = unknown> = DeclarativeConfig<TContext, TSelect, TInsert, TUpdate> | FactoryConfig<TContext, TSelect, TInsert, TUpdate> | ProceduresConfig<TContext, TSelect, TInsert, TUpdate>;
|
|
344
382
|
interface CrudProcedures<TSelect, TInsert, TUpdate> {
|
|
345
383
|
list: AnyProcedure;
|
|
346
384
|
get: AnyProcedure;
|
|
@@ -431,4 +469,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
431
469
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
432
470
|
type AppRouter = typeof appRouter;
|
|
433
471
|
//#endregion
|
|
434
|
-
export { type AnyProcedure, type AppRouter, type CreateMiddlewareParams, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type
|
|
472
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
|
|
3
|
-
import { initTRPC } from "@trpc/server";
|
|
3
|
+
import { TRPCError, initTRPC } from "@trpc/server";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { addDays, endOfDay, startOfDay } from "date-fns";
|
|
6
6
|
|
|
@@ -389,38 +389,53 @@ function resolveSoftDelete(option) {
|
|
|
389
389
|
getValue: config.value ?? (() => /* @__PURE__ */ new Date())
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
392
|
+
/**
|
|
393
|
+
* 判断 procedure 配置的类型
|
|
394
|
+
*/
|
|
395
|
+
function isProcedureMap(config) {
|
|
396
|
+
if (!config) return false;
|
|
397
|
+
if (typeof config === "function") return false;
|
|
398
|
+
if (typeof config === "object" && config !== null) {
|
|
399
|
+
const keys = Object.keys(config);
|
|
400
|
+
const validKeys = [
|
|
401
|
+
"list",
|
|
402
|
+
"get",
|
|
403
|
+
"create",
|
|
404
|
+
"update",
|
|
405
|
+
"delete",
|
|
406
|
+
"deleteMany",
|
|
407
|
+
"updateMany",
|
|
408
|
+
"upsert",
|
|
409
|
+
"default"
|
|
410
|
+
];
|
|
411
|
+
return keys.some((k) => validKeys.includes(k));
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
function isProcedureFactory(config) {
|
|
416
|
+
return typeof config === "function";
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* 解析 procedure 配置为工厂函数
|
|
420
|
+
*/
|
|
421
|
+
function resolveProcedureFactory(config) {
|
|
422
|
+
if (!config) return () => publicProcedure;
|
|
423
|
+
if (isProcedureFactory(config)) return config;
|
|
424
|
+
if (isProcedureMap(config)) {
|
|
425
|
+
const map = config;
|
|
426
|
+
const defaultProc = map.default ?? publicProcedure;
|
|
427
|
+
return (op) => map[op] ?? defaultProc;
|
|
414
428
|
}
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
return () => config;
|
|
430
|
+
}
|
|
431
|
+
function resolveConfig(config) {
|
|
417
432
|
return {
|
|
418
|
-
procedureFactory: ()
|
|
419
|
-
guard:
|
|
420
|
-
getScope: (ctx, tbl, op) =>
|
|
421
|
-
getInject: (ctx, op) =>
|
|
433
|
+
procedureFactory: resolveProcedureFactory(config.procedure),
|
|
434
|
+
guard: config.guard,
|
|
435
|
+
getScope: (ctx, tbl, op) => config.scope?.(ctx, tbl, op),
|
|
436
|
+
getInject: (ctx, op) => config.inject?.(ctx, op) ?? {},
|
|
422
437
|
checkAuthorize: async (ctx, resource, op) => {
|
|
423
|
-
if (
|
|
438
|
+
if (config.authorize) return config.authorize(ctx, resource, op);
|
|
424
439
|
return true;
|
|
425
440
|
}
|
|
426
441
|
};
|
|
@@ -465,7 +480,7 @@ function createCrudRouter(config) {
|
|
|
465
480
|
const conditions = [];
|
|
466
481
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
467
482
|
if (scopeCondition) conditions.push(scopeCondition);
|
|
468
|
-
if (softDelete
|
|
483
|
+
if (softDelete) {
|
|
469
484
|
const col = getSoftDeleteColumn();
|
|
470
485
|
if (col) conditions.push(isNull(col));
|
|
471
486
|
}
|
|
@@ -479,15 +494,24 @@ function createCrudRouter(config) {
|
|
|
479
494
|
if (operation === "upsert") {
|
|
480
495
|
const canCreate = await resolved.guard(ctx, "create");
|
|
481
496
|
const canUpdate = await resolved.guard(ctx, "update");
|
|
482
|
-
if (!canCreate || !canUpdate) throw new
|
|
497
|
+
if (!canCreate || !canUpdate) throw new TRPCError({
|
|
498
|
+
code: "FORBIDDEN",
|
|
499
|
+
message: "Forbidden: upsert requires both create and update permissions"
|
|
500
|
+
});
|
|
483
501
|
return;
|
|
484
502
|
}
|
|
485
|
-
if (!await resolved.guard(ctx, operation)) throw new
|
|
503
|
+
if (!await resolved.guard(ctx, operation)) throw new TRPCError({
|
|
504
|
+
code: "FORBIDDEN",
|
|
505
|
+
message: `Forbidden: ${operation} not allowed`
|
|
506
|
+
});
|
|
486
507
|
}
|
|
487
508
|
};
|
|
488
509
|
const withAuthorize = async (ctx, resource, operation) => {
|
|
489
510
|
if (!resource) return;
|
|
490
|
-
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new
|
|
511
|
+
if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new TRPCError({
|
|
512
|
+
code: "FORBIDDEN",
|
|
513
|
+
message: `Forbidden: Cannot ${operation} this resource`
|
|
514
|
+
});
|
|
491
515
|
};
|
|
492
516
|
const procedures = {
|
|
493
517
|
list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
|
|
@@ -570,7 +594,10 @@ function createCrudRouter(config) {
|
|
|
570
594
|
const where = buildWhere(ctx, "update", eq(getIdColumn(), input.id));
|
|
571
595
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
572
596
|
await withAuthorize(ctx, existing, "update");
|
|
573
|
-
if (!existing) throw new
|
|
597
|
+
if (!existing) throw new TRPCError({
|
|
598
|
+
code: "NOT_FOUND",
|
|
599
|
+
message: "Resource not found or access denied"
|
|
600
|
+
});
|
|
574
601
|
const doUpdate = async (updateData) => {
|
|
575
602
|
const injectData = resolved.getInject(ctx, "update");
|
|
576
603
|
const data = {
|
|
@@ -594,7 +621,10 @@ function createCrudRouter(config) {
|
|
|
594
621
|
const where = buildWhere(ctx, "delete", eq(getIdColumn(), input));
|
|
595
622
|
const [existing] = await ctx.db.select().from(table).where(where);
|
|
596
623
|
await withAuthorize(ctx, existing, "delete");
|
|
597
|
-
if (!existing) throw new
|
|
624
|
+
if (!existing) throw new TRPCError({
|
|
625
|
+
code: "NOT_FOUND",
|
|
626
|
+
message: "Resource not found or access denied"
|
|
627
|
+
});
|
|
598
628
|
const doDelete = async () => {
|
|
599
629
|
let deleted;
|
|
600
630
|
if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
@@ -613,8 +643,8 @@ function createCrudRouter(config) {
|
|
|
613
643
|
await withGuard(ctx, "deleteMany");
|
|
614
644
|
const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
|
|
615
645
|
const doDeleteMany = async () => {
|
|
616
|
-
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
|
|
617
|
-
else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
|
|
646
|
+
if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
|
|
647
|
+
else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
|
|
618
648
|
};
|
|
619
649
|
if (m.deleteMany) return m.deleteMany({
|
|
620
650
|
ctx,
|
|
@@ -635,7 +665,7 @@ function createCrudRouter(config) {
|
|
|
635
665
|
...updateData,
|
|
636
666
|
...injectData
|
|
637
667
|
};
|
|
638
|
-
return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
|
|
668
|
+
return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
|
|
639
669
|
};
|
|
640
670
|
if (m.updateMany) return m.updateMany({
|
|
641
671
|
ctx,
|
|
@@ -648,21 +678,27 @@ function createCrudRouter(config) {
|
|
|
648
678
|
upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
|
|
649
679
|
await withGuard(ctx, "upsert");
|
|
650
680
|
const doUpsert = async (inputData) => {
|
|
651
|
-
const injectData = resolved.getInject(ctx, "create");
|
|
652
|
-
const data = {
|
|
653
|
-
...inputData,
|
|
654
|
-
...injectData
|
|
655
|
-
};
|
|
656
681
|
const inputId = inputData[idField];
|
|
657
682
|
let isNew = true;
|
|
683
|
+
let existing = null;
|
|
658
684
|
if (inputId) {
|
|
659
685
|
const where = buildWhere(ctx, "get", eq(getIdColumn(), inputId));
|
|
660
|
-
const [
|
|
686
|
+
const [found] = await ctx.db.select().from(table).where(where);
|
|
687
|
+
existing = found;
|
|
661
688
|
isNew = !existing;
|
|
689
|
+
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
662
690
|
}
|
|
691
|
+
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
692
|
+
const data = {
|
|
693
|
+
...inputData,
|
|
694
|
+
...injectData
|
|
695
|
+
};
|
|
663
696
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
664
697
|
target: getIdColumn(),
|
|
665
|
-
set:
|
|
698
|
+
set: {
|
|
699
|
+
...inputData,
|
|
700
|
+
...resolved.getInject(ctx, "update")
|
|
701
|
+
}
|
|
666
702
|
}).returning();
|
|
667
703
|
return {
|
|
668
704
|
data: result,
|
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.7.0",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"superjson": "^2.2.6",
|
|
46
46
|
"tsdown": "^0.15.12",
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.0.18",
|
|
48
49
|
"zod": "^4.1.13",
|
|
49
50
|
"@internal/eslint-config": "0.3.0",
|
|
50
|
-
"@internal/prettier-config": "0.0.1",
|
|
51
51
|
"@internal/tsconfig": "0.1.0",
|
|
52
|
+
"@internal/prettier-config": "0.0.1",
|
|
52
53
|
"@internal/vitest-config": "0.1.0",
|
|
53
54
|
"@internal/tsdown-config": "0.1.0"
|
|
54
55
|
},
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"build:watch": "tsdown --watch",
|
|
61
62
|
"test": "vitest --run --passWithNoTests",
|
|
62
63
|
"test:watch": "vitest --watch",
|
|
64
|
+
"test:coverage": "vitest --run --coverage",
|
|
63
65
|
"typecheck": "tsc --noEmit",
|
|
64
66
|
"lint": "eslint",
|
|
65
67
|
"format": "prettier --check . --ignore-path ../../.gitignore"
|