@wordrhyme/auto-crud-server 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,7 +136,7 @@ export { handler as GET, handler as POST };
136
136
 
137
137
  ## 📖 自动生成的路由
138
138
 
139
- `createCrudRouter` 会自动生成以下 6 个路由:
139
+ `createCrudRouter` 会自动生成以下 7 个路由:
140
140
 
141
141
  ### 1. `list` - 列表查询
142
142
 
@@ -276,6 +276,29 @@ void
276
276
  await trpc.tasks.deleteMany({ ids: ["1", "2", "3"] });
277
277
  ```
278
278
 
279
+ ### 7. `updateMany` - 批量更新
280
+
281
+ **输入**:
282
+ ```typescript
283
+ {
284
+ ids: string[];
285
+ data: Partial<Omit<Task, "id" | "createdAt" | "updatedAt">>;
286
+ }
287
+ ```
288
+
289
+ **输出**:
290
+ ```typescript
291
+ { updated: number }
292
+ ```
293
+
294
+ **示例**:
295
+ ```typescript
296
+ await trpc.tasks.updateMany({
297
+ ids: ["1", "2", "3"],
298
+ data: { status: "done" },
299
+ });
300
+ ```
301
+
279
302
  ---
280
303
 
281
304
  ## 🎯 高级过滤
@@ -392,13 +415,21 @@ interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
392
415
  #### 返回值
393
416
 
394
417
  ```typescript
418
+ // 返回增强的 tRPC router,带有 .procedures 属性
395
419
  {
396
- list: Procedure<ListInput, ListOutput>,
397
- get: Procedure<{ id: string }, TSelect>,
398
- create: Procedure<TInsert, TSelect>,
399
- update: Procedure<{ id: string, data: TUpdate }, TSelect>,
400
- delete: Procedure<{ id: string }, void>,
401
- deleteMany: Procedure<{ ids: string[] }, void>,
420
+ // 标准 tRPC router(可直接嵌套到 appRouter)
421
+ ...routerMethods,
422
+
423
+ // spread procedures 对象(用于扩展自定义路由)
424
+ procedures: {
425
+ list: Procedure<ListInput, ListOutput>,
426
+ getById: Procedure<string, TSelect>,
427
+ create: Procedure<TInsert, TSelect>,
428
+ update: Procedure<{ id: string, data: TUpdate }, TSelect>,
429
+ delete: Procedure<string, TSelect>,
430
+ deleteMany: Procedure<string[], { deleted: number }>,
431
+ updateMany: Procedure<{ ids: string[], data: TUpdate }, { updated: number }>,
432
+ }
402
433
  }
403
434
  ```
404
435
 
@@ -487,24 +518,29 @@ const result = await db
487
518
 
488
519
  ## 🎨 自定义扩展
489
520
 
490
- ### 添加自定义路由
521
+ ### 添加自定义路由(推荐方式)
522
+
523
+ 使用 `.procedures` 属性 spread 出 CRUD 路由,然后添加自定义路由:
491
524
 
492
525
  ```typescript
493
- import { createCrudRouter } from "@wordrhyme/auto-crud-server";
494
- import { publicProcedure } from "../trpc";
526
+ import { createCrudRouter, router } from "@wordrhyme/auto-crud-server";
527
+ import { protectedProcedure } from "../trpc";
528
+ import { z } from "zod";
495
529
 
496
- const baseCrudRouter = createCrudRouter({
530
+ // 创建基础 CRUD
531
+ const tasksCrud = createCrudRouter({
497
532
  table: tasks,
498
533
  selectSchema: selectTaskSchema,
499
534
  insertSchema: insertTaskSchema,
500
535
  updateSchema: updateTaskSchema,
501
536
  });
502
537
 
503
- export const tasksRouter = {
504
- ...baseCrudRouter,
538
+ // 使用 .procedures spread 并添加自定义路由
539
+ export const tasksRouter = router({
540
+ ...tasksCrud.procedures,
505
541
 
506
- // 添加自定义路由
507
- archive: publicProcedure
542
+ // 自定义路由:归档
543
+ archive: protectedProcedure
508
544
  .input(z.object({ id: z.string() }))
509
545
  .mutation(async ({ input, ctx }) => {
510
546
  return ctx.db
@@ -513,7 +549,8 @@ export const tasksRouter = {
513
549
  .where(eq(tasks.id, input.id));
514
550
  }),
515
551
 
516
- unarchive: publicProcedure
552
+ // 自定义路由:取消归档
553
+ unarchive: protectedProcedure
517
554
  .input(z.object({ id: z.string() }))
518
555
  .mutation(async ({ input, ctx }) => {
519
556
  return ctx.db
@@ -521,30 +558,52 @@ export const tasksRouter = {
521
558
  .set({ archived: false })
522
559
  .where(eq(tasks.id, input.id));
523
560
  }),
524
- };
561
+ });
562
+ ```
563
+
564
+ ### 直接使用(不需要扩展时)
565
+
566
+ 如果不需要添加自定义路由,可以直接使用返回的 router:
567
+
568
+ ```typescript
569
+ const tasksCrud = createCrudRouter({
570
+ table: tasks,
571
+ selectSchema: selectTaskSchema,
572
+ insertSchema: insertTaskSchema,
573
+ updateSchema: updateTaskSchema,
574
+ });
575
+
576
+ // 直接嵌套到 appRouter
577
+ export const appRouter = router({
578
+ tasks: tasksCrud,
579
+ });
525
580
  ```
526
581
 
527
582
  ### 自定义过滤逻辑
528
583
 
529
584
  ```typescript
530
- // createCrudRouter 之前预处理过滤条件
531
- const customList = publicProcedure
532
- .input(listInputSchema)
533
- .query(async ({ input, ctx }) => {
534
- // 自定义过滤逻辑
535
- const customFilters = input.filters?.map(filter => {
536
- if (filter.id === "customField") {
537
- // 自定义处理
538
- return { ...filter, operator: "custom" };
539
- }
540
- return filter;
541
- });
542
-
543
- return baseCrudRouter.list({
544
- ...input,
545
- filters: customFilters,
546
- }, ctx);
547
- });
585
+ // 覆盖 list 路由,添加自定义过滤逻辑
586
+ const tasksCrud = createCrudRouter({ ... });
587
+
588
+ export const tasksRouter = router({
589
+ ...tasksCrud.procedures,
590
+
591
+ // 覆盖 list,添加自定义逻辑
592
+ list: protectedProcedure
593
+ .input(listInputSchema)
594
+ .query(async ({ input, ctx }) => {
595
+ // 自定义过滤逻辑
596
+ const customFilters = input.filters?.map(filter => {
597
+ if (filter.id === "customField") {
598
+ return { ...filter, operator: "custom" };
599
+ }
600
+ return filter;
601
+ });
602
+
603
+ // 调用原始 list(需要手动实现或使用 db 查询)
604
+ return ctx.db.select().from(tasks).where(...);
605
+ }),
606
+ });
548
607
  ```
549
608
 
550
609
  ---
package/dist/index.cjs CHANGED
@@ -401,9 +401,58 @@ const dataTableConfig = {
401
401
 
402
402
  //#endregion
403
403
  //#region src/routers/_factory.ts
404
- /**
405
- * 过滤器项 Schema
406
- */
404
+ function resolveSoftDelete(option) {
405
+ if (!option) return null;
406
+ if (option === true) return {
407
+ column: "deletedAt",
408
+ getValue: () => /* @__PURE__ */ new Date()
409
+ };
410
+ if (typeof option === "string") return {
411
+ column: option,
412
+ getValue: () => /* @__PURE__ */ new Date()
413
+ };
414
+ const config = option;
415
+ return {
416
+ column: config.column,
417
+ getValue: config.value ?? (() => /* @__PURE__ */ new Date())
418
+ };
419
+ }
420
+ function resolveConfig(config) {
421
+ const mode = config.mode || "declarative";
422
+ if (mode === "factory") {
423
+ const factoryConfig = config;
424
+ return {
425
+ procedureFactory: factoryConfig.procedureFactory,
426
+ guard: void 0,
427
+ getScope: () => void 0,
428
+ getInject: (ctx, op) => factoryConfig.inject?.(ctx, op) ?? {},
429
+ checkAuthorize: async () => true
430
+ };
431
+ }
432
+ if (mode === "procedures") {
433
+ const proceduresConfig = config;
434
+ const defaultProc = proceduresConfig.defaultProcedure ?? publicProcedure;
435
+ return {
436
+ procedureFactory: (op) => proceduresConfig.procedures[op] ?? defaultProc,
437
+ guard: void 0,
438
+ getScope: () => void 0,
439
+ getInject: (ctx, op) => proceduresConfig.inject?.(ctx, op) ?? {},
440
+ checkAuthorize: async () => true
441
+ };
442
+ }
443
+ const declarativeConfig = config;
444
+ const baseProcedure = declarativeConfig.procedure ?? publicProcedure;
445
+ return {
446
+ procedureFactory: () => baseProcedure,
447
+ guard: declarativeConfig.guard,
448
+ getScope: (ctx, tbl, op) => declarativeConfig.scope?.(ctx, tbl, op),
449
+ getInject: (ctx, op) => declarativeConfig.inject?.(ctx, op) ?? {},
450
+ checkAuthorize: async (ctx, resource, op) => {
451
+ if (declarativeConfig.authorize) return declarativeConfig.authorize(ctx, resource, op);
452
+ return true;
453
+ }
454
+ };
455
+ }
407
456
  const filterItemSchema = zod.z.object({
408
457
  id: zod.z.string(),
409
458
  value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
@@ -411,9 +460,6 @@ const filterItemSchema = zod.z.object({
411
460
  operator: zod.z.enum(dataTableConfig.operators),
412
461
  filterId: zod.z.string()
413
462
  });
414
- /**
415
- * 列表查询输入 Schema
416
- */
417
463
  const listInputSchema = zod.z.object({
418
464
  page: zod.z.number().min(1).default(1),
419
465
  perPage: zod.z.number().min(1).max(100).default(10),
@@ -424,42 +470,53 @@ const listInputSchema = zod.z.object({
424
470
  filters: zod.z.array(filterItemSchema).optional(),
425
471
  joinOperator: zod.z.enum(["and", "or"]).default("and")
426
472
  });
427
- /**
428
- * 验证列是否在白名单中
429
- */
430
- function validateColumn(columnId, allowedColumns, columnType) {
473
+ function validateColumn(columnId, allowedColumns) {
431
474
  if (!allowedColumns || allowedColumns.length === 0) return true;
432
475
  return allowedColumns.includes(columnId);
433
476
  }
434
- /**
435
- * 创建通用 CRUD Router
436
- */
437
477
  function createCrudRouter(config) {
438
- const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, procedure: baseProcedure = publicProcedure, authorize } = config;
478
+ const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption } = config;
479
+ const resolved = resolveConfig(config);
480
+ const softDelete = resolveSoftDelete(softDeleteOption);
439
481
  const getIdColumn = () => table[idField];
440
- const withAuthorize = async (operation, input) => {
441
- if (authorize) {
442
- if (!await authorize({
443
- operation,
444
- input
445
- })) throw new Error(`Unauthorized: ${operation} operation not allowed`);
482
+ const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
483
+ const buildWhere = (ctx, operation, additionalCondition) => {
484
+ const conditions = [];
485
+ const scopeCondition = resolved.getScope(ctx, table, operation);
486
+ if (scopeCondition) conditions.push(scopeCondition);
487
+ if (softDelete && (operation === "list" || operation === "get")) {
488
+ const col = getSoftDeleteColumn();
489
+ if (col) conditions.push((0, drizzle_orm.isNull)(col));
490
+ }
491
+ if (additionalCondition) conditions.push(additionalCondition);
492
+ if (conditions.length === 0) return void 0;
493
+ if (conditions.length === 1) return conditions[0];
494
+ return (0, drizzle_orm.and)(...conditions);
495
+ };
496
+ const withGuard = async (ctx, operation) => {
497
+ if (resolved.guard) {
498
+ if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
446
499
  }
447
500
  };
448
- return router({
449
- list: baseProcedure.input(listInputSchema).query(async ({ ctx, input }) => {
450
- await withAuthorize("list", input);
501
+ const withAuthorize = async (ctx, resource, operation) => {
502
+ if (!resource) return;
503
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
504
+ };
505
+ const procedures = {
506
+ list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
507
+ await withGuard(ctx, "list");
451
508
  const offset = (input.page - 1) * input.perPage;
452
- const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns, "filter"));
453
- const where = validatedFilters?.length ? filterColumns({
509
+ const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
510
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
454
511
  table,
455
512
  filters: validatedFilters,
456
513
  joinOperator: input.joinOperator
457
- }) : void 0;
514
+ }) : void 0);
458
515
  let query = ctx.db.select().from(table).$dynamic();
459
516
  if (where) query = query.where(where);
460
517
  if (input.sort?.length) {
461
518
  const sortField = input.sort[0];
462
- if (sortField && validateColumn(sortField.id, sortableColumns, "sort")) {
519
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
463
520
  const column = table[sortField.id];
464
521
  if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
465
522
  }
@@ -476,46 +533,75 @@ function createCrudRouter(config) {
476
533
  pageCount: Math.ceil(count / input.perPage)
477
534
  };
478
535
  }),
479
- getById: baseProcedure.input(zod.z.string()).query(async ({ ctx, input }) => {
480
- await withAuthorize("get", input);
481
- const column = getIdColumn();
482
- const [item] = await ctx.db.select().from(table).where((0, drizzle_orm.eq)(column, input));
536
+ getById: resolved.procedureFactory("get").input(zod.z.string()).query(async ({ ctx, input }) => {
537
+ await withGuard(ctx, "get");
538
+ const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), input));
539
+ const [item] = await ctx.db.select().from(table).where(where);
540
+ await withAuthorize(ctx, item, "get");
483
541
  return item ?? null;
484
542
  }),
485
- create: baseProcedure.input(insertSchema).mutation(async ({ ctx, input }) => {
486
- await withAuthorize("create", input);
487
- const [created] = await ctx.db.insert(table).values(input).returning();
543
+ create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
544
+ await withGuard(ctx, "create");
545
+ const injectData = resolved.getInject(ctx, "create");
546
+ const data = {
547
+ ...input,
548
+ ...injectData
549
+ };
550
+ const [created] = await ctx.db.insert(table).values(data).returning();
488
551
  return created;
489
552
  }),
490
- update: baseProcedure.input(zod.z.object({
553
+ update: resolved.procedureFactory("update").input(zod.z.object({
491
554
  id: zod.z.string(),
492
555
  data: updateSchema
493
556
  })).mutation(async ({ ctx, input }) => {
494
- await withAuthorize("update", input);
495
- const column = getIdColumn();
496
- const [updated] = await ctx.db.update(table).set(input.data).where((0, drizzle_orm.eq)(column, input.id)).returning();
557
+ await withGuard(ctx, "update");
558
+ const where = buildWhere(ctx, "update", (0, drizzle_orm.eq)(getIdColumn(), input.id));
559
+ const [existing] = await ctx.db.select().from(table).where(where);
560
+ await withAuthorize(ctx, existing, "update");
561
+ if (!existing) throw new Error("Resource not found or access denied");
562
+ const injectData = resolved.getInject(ctx, "update");
563
+ const data = {
564
+ ...input.data,
565
+ ...injectData
566
+ };
567
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
497
568
  return updated;
498
569
  }),
499
- delete: baseProcedure.input(zod.z.string()).mutation(async ({ ctx, input }) => {
500
- await withAuthorize("delete", input);
501
- const column = getIdColumn();
502
- const [deleted] = await ctx.db.delete(table).where((0, drizzle_orm.eq)(column, input)).returning();
570
+ delete: resolved.procedureFactory("delete").input(zod.z.string()).mutation(async ({ ctx, input }) => {
571
+ await withGuard(ctx, "delete");
572
+ const where = buildWhere(ctx, "delete", (0, drizzle_orm.eq)(getIdColumn(), input));
573
+ const [existing] = await ctx.db.select().from(table).where(where);
574
+ await withAuthorize(ctx, existing, "delete");
575
+ if (!existing) throw new Error("Resource not found or access denied");
576
+ if (softDelete) {
577
+ const [deleted$1] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
578
+ return deleted$1;
579
+ }
580
+ const [deleted] = await ctx.db.delete(table).where(where).returning();
503
581
  return deleted;
504
582
  }),
505
- deleteMany: baseProcedure.input(zod.z.array(zod.z.string()).max(maxBatchSize, `Maximum ${maxBatchSize} items allowed`)).mutation(async ({ ctx, input }) => {
506
- await withAuthorize("deleteMany", input);
507
- const column = getIdColumn();
508
- return { deleted: (await ctx.db.delete(table).where((0, drizzle_orm.inArray)(column, input)).returning()).length };
583
+ deleteMany: resolved.procedureFactory("deleteMany").input(zod.z.array(zod.z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
584
+ await withGuard(ctx, "deleteMany");
585
+ const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
586
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
587
+ return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
509
588
  }),
510
- updateMany: baseProcedure.input(zod.z.object({
511
- ids: zod.z.array(zod.z.string()).max(maxBatchSize, `Maximum ${maxBatchSize} items allowed`),
589
+ updateMany: resolved.procedureFactory("updateMany").input(zod.z.object({
590
+ ids: zod.z.array(zod.z.string()).max(maxBatchSize),
512
591
  data: updateSchema
513
592
  })).mutation(async ({ ctx, input }) => {
514
- await withAuthorize("updateMany", input);
515
- const column = getIdColumn();
516
- return { updated: (await ctx.db.update(table).set(input.data).where((0, drizzle_orm.inArray)(column, input.ids)).returning()).length };
593
+ await withGuard(ctx, "updateMany");
594
+ const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
595
+ const injectData = resolved.getInject(ctx, "update");
596
+ const data = {
597
+ ...input.data,
598
+ ...injectData
599
+ };
600
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
517
601
  })
518
- });
602
+ };
603
+ const crudRouter = router(procedures);
604
+ return Object.assign(crudRouter, { procedures });
519
605
  }
520
606
 
521
607
  //#endregion