@wordrhyme/auto-crud-server 0.4.0 → 0.6.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.js CHANGED
@@ -447,10 +447,10 @@ function validateColumn(columnId, allowedColumns) {
447
447
  return allowedColumns.includes(columnId);
448
448
  }
449
449
  function createCrudRouter(config) {
450
- const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, hooks = {} } = config;
450
+ const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption, middleware = {} } = config;
451
451
  const resolved = resolveConfig(config);
452
452
  const softDelete = resolveSoftDelete(softDeleteOption);
453
- const h = hooks;
453
+ const m = middleware;
454
454
  const getIdColumn = () => table[idField];
455
455
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
456
456
  const buildWhere = (ctx, operation, additionalCondition) => {
@@ -478,72 +478,75 @@ function createCrudRouter(config) {
478
478
  const procedures = {
479
479
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
480
480
  await withGuard(ctx, "list");
481
- let processedInput = input;
482
- if (h.beforeList) {
483
- const result = await h.beforeList(ctx, processedInput);
484
- if (result) processedInput = result;
485
- }
486
- const offset = (processedInput.page - 1) * processedInput.perPage;
487
- const validatedFilters = processedInput.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
488
- const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
489
- table,
490
- filters: validatedFilters,
491
- joinOperator: processedInput.joinOperator
492
- }) : void 0);
493
- let query = ctx.db.select().from(table).$dynamic();
494
- if (where) query = query.where(where);
495
- if (processedInput.sort?.length) {
496
- const sortField = processedInput.sort[0];
497
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
498
- const column = table[sortField.id];
499
- if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
481
+ const doList = async (listInput$1) => {
482
+ const offset = (listInput$1.page - 1) * listInput$1.perPage;
483
+ const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
484
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
485
+ table,
486
+ filters: validatedFilters,
487
+ joinOperator: listInput$1.joinOperator
488
+ }) : void 0);
489
+ let query = ctx.db.select().from(table).$dynamic();
490
+ if (where) query = query.where(where);
491
+ if (listInput$1.sort?.length) {
492
+ const sortField = listInput$1.sort[0];
493
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
494
+ const column = table[sortField.id];
495
+ if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
496
+ }
500
497
  }
501
- }
502
- const data = await query.limit(processedInput.perPage).offset(offset);
503
- let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
504
- if (where) countQuery = countQuery.where(where);
505
- const count = (await countQuery)[0]?.count ?? 0;
506
- let listResult = {
507
- data,
508
- total: count,
509
- page: processedInput.page,
510
- perPage: processedInput.perPage,
511
- pageCount: Math.ceil(count / processedInput.perPage)
498
+ const data = await query.limit(listInput$1.perPage).offset(offset);
499
+ let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
500
+ if (where) countQuery = countQuery.where(where);
501
+ const count = (await countQuery)[0]?.count ?? 0;
502
+ return {
503
+ data,
504
+ total: count,
505
+ page: listInput$1.page,
506
+ perPage: listInput$1.perPage,
507
+ pageCount: Math.ceil(count / listInput$1.perPage)
508
+ };
512
509
  };
513
- if (h.afterList) {
514
- const result = await h.afterList(ctx, listResult);
515
- if (result) listResult = result;
516
- }
517
- return listResult;
510
+ const listInput = input;
511
+ if (m.list) return m.list({
512
+ ctx,
513
+ input: listInput,
514
+ next: async (modifiedInput) => doList(modifiedInput ?? listInput)
515
+ });
516
+ return doList(listInput);
518
517
  }),
519
518
  getById: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
520
519
  await withGuard(ctx, "get");
521
- if (h.beforeGet) await h.beforeGet(ctx, input);
522
- const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
523
- const [item] = await ctx.db.select().from(table).where(where);
524
- await withAuthorize(ctx, item, "get");
525
- let result = item ?? null;
526
- if (h.afterGet) {
527
- const hookResult = await h.afterGet(ctx, result);
528
- if (hookResult !== void 0) result = hookResult;
529
- }
530
- return result;
520
+ const doGet = async () => {
521
+ const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
522
+ const [item] = await ctx.db.select().from(table).where(where);
523
+ await withAuthorize(ctx, item, "get");
524
+ return item ?? null;
525
+ };
526
+ if (m.get) return m.get({
527
+ ctx,
528
+ id: input,
529
+ next: doGet
530
+ });
531
+ return doGet();
531
532
  }),
532
533
  create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
533
534
  await withGuard(ctx, "create");
534
- let processedData = input;
535
- if (h.beforeCreate) {
536
- const result = await h.beforeCreate(ctx, processedData);
537
- if (result) processedData = result;
538
- }
539
- const injectData = resolved.getInject(ctx, "create");
540
- const data = {
541
- ...processedData,
542
- ...injectData
535
+ const doCreate = async (inputData) => {
536
+ const injectData = resolved.getInject(ctx, "create");
537
+ const data = {
538
+ ...inputData,
539
+ ...injectData
540
+ };
541
+ const [created] = await ctx.db.insert(table).values(data).returning();
542
+ return created;
543
543
  };
544
- const [created] = await ctx.db.insert(table).values(data).returning();
545
- if (h.afterCreate) await h.afterCreate(ctx, created);
546
- return created;
544
+ if (m.create) return m.create({
545
+ ctx,
546
+ input,
547
+ next: async (modifiedInput) => doCreate(modifiedInput ?? input)
548
+ });
549
+ return doCreate(input);
547
550
  }),
548
551
  update: resolved.procedureFactory("update").input(z.object({
549
552
  id: z.string(),
@@ -554,19 +557,23 @@ function createCrudRouter(config) {
554
557
  const [existing] = await ctx.db.select().from(table).where(where);
555
558
  await withAuthorize(ctx, existing, "update");
556
559
  if (!existing) throw new Error("Resource not found or access denied");
557
- let processedData = input.data;
558
- if (h.beforeUpdate) {
559
- const result = await h.beforeUpdate(ctx, input.id, processedData, existing);
560
- if (result) processedData = result;
561
- }
562
- const injectData = resolved.getInject(ctx, "update");
563
- const data = {
564
- ...processedData,
565
- ...injectData
560
+ const doUpdate = async (updateData) => {
561
+ const injectData = resolved.getInject(ctx, "update");
562
+ const data = {
563
+ ...updateData,
564
+ ...injectData
565
+ };
566
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
567
+ return updated;
566
568
  };
567
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
568
- if (h.afterUpdate) await h.afterUpdate(ctx, updated);
569
- return updated;
569
+ if (m.update) return m.update({
570
+ ctx,
571
+ id: input.id,
572
+ data: input.data,
573
+ existing,
574
+ next: async (modifiedData) => doUpdate(modifiedData ?? input.data)
575
+ });
576
+ return doUpdate(input.data);
570
577
  }),
571
578
  delete: resolved.procedureFactory("delete").input(z.string()).mutation(async ({ ctx, input }) => {
572
579
  await withGuard(ctx, "delete");
@@ -574,51 +581,179 @@ function createCrudRouter(config) {
574
581
  const [existing] = await ctx.db.select().from(table).where(where);
575
582
  await withAuthorize(ctx, existing, "delete");
576
583
  if (!existing) throw new Error("Resource not found or access denied");
577
- if (h.beforeDelete) await h.beforeDelete(ctx, input, existing);
578
- let deleted;
579
- if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
580
- else [deleted] = await ctx.db.delete(table).where(where).returning();
581
- if (h.afterDelete) await h.afterDelete(ctx, deleted);
582
- return deleted;
584
+ const doDelete = async () => {
585
+ let deleted;
586
+ if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
587
+ else [deleted] = await ctx.db.delete(table).where(where).returning();
588
+ return deleted;
589
+ };
590
+ if (m.delete) return m.delete({
591
+ ctx,
592
+ id: input,
593
+ existing,
594
+ next: doDelete
595
+ });
596
+ return doDelete();
583
597
  }),
584
598
  deleteMany: resolved.procedureFactory("deleteMany").input(z.array(z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
585
599
  await withGuard(ctx, "deleteMany");
586
- if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
587
600
  const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
588
- let result;
589
- if (softDelete) result = { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
590
- else result = { deleted: (await ctx.db.delete(table).where(where).returning()).length };
591
- if (h.afterDeleteMany) await h.afterDeleteMany(ctx, result);
592
- return result;
601
+ const doDeleteMany = async () => {
602
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
603
+ else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
604
+ };
605
+ if (m.deleteMany) return m.deleteMany({
606
+ ctx,
607
+ ids: input,
608
+ next: doDeleteMany
609
+ });
610
+ return doDeleteMany();
593
611
  }),
594
612
  updateMany: resolved.procedureFactory("updateMany").input(z.object({
595
613
  ids: z.array(z.string()).max(maxBatchSize),
596
614
  data: updateSchema
597
615
  })).mutation(async ({ ctx, input }) => {
598
616
  await withGuard(ctx, "updateMany");
599
- let processedData = input.data;
600
- if (h.beforeUpdateMany) {
601
- const result$1 = await h.beforeUpdateMany(ctx, input.ids, processedData);
602
- if (result$1) processedData = result$1;
603
- }
604
617
  const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
605
- const injectData = resolved.getInject(ctx, "update");
606
- const data = {
607
- ...processedData,
608
- ...injectData
618
+ const doUpdateMany = async (updateData) => {
619
+ const injectData = resolved.getInject(ctx, "update");
620
+ const data = {
621
+ ...updateData,
622
+ ...injectData
623
+ };
624
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
609
625
  };
610
- const result = { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
611
- if (h.afterUpdateMany) await h.afterUpdateMany(ctx, result);
612
- return result;
626
+ if (m.updateMany) return m.updateMany({
627
+ ctx,
628
+ ids: input.ids,
629
+ data: input.data,
630
+ next: async (modifiedData) => doUpdateMany(modifiedData ?? input.data)
631
+ });
632
+ return doUpdateMany(input.data);
613
633
  })
614
634
  };
615
635
  const crudRouter = router(procedures);
616
636
  return Object.assign(crudRouter, { procedures });
617
637
  }
618
638
 
639
+ //#endregion
640
+ //#region src/lib/middleware-helpers.ts
641
+ /**
642
+ * 创建 after-only 的 middleware(最常见场景)
643
+ * 在操作完成后执行副作用,不修改返回值
644
+ *
645
+ * @example
646
+ * middleware: {
647
+ * create: afterMiddleware(async (ctx, result) => {
648
+ * await sendEmail(result);
649
+ * await logAudit(ctx.user, 'create', result);
650
+ * }),
651
+ * }
652
+ */
653
+ function afterMiddleware(fn) {
654
+ return async ({ ctx, next }) => {
655
+ const result = await next();
656
+ await fn(ctx, result);
657
+ return result;
658
+ };
659
+ }
660
+ /**
661
+ * 创建 after-only 的 middleware,可修改返回值
662
+ *
663
+ * @example
664
+ * middleware: {
665
+ * get: afterMiddlewareTransform(async (ctx, result) => {
666
+ * return { ...result, computed: calculateSomething(result) };
667
+ * }),
668
+ * }
669
+ */
670
+ function afterMiddlewareTransform(fn) {
671
+ return async ({ ctx, next }) => {
672
+ return fn(ctx, await next());
673
+ };
674
+ }
675
+ /**
676
+ * 创建 before-only 的 middleware
677
+ * 在操作执行前修改输入数据
678
+ *
679
+ * @example
680
+ * middleware: {
681
+ * create: beforeMiddleware(async (ctx, input) => {
682
+ * return { ...input, slug: slugify(input.title), createdBy: ctx.user.id };
683
+ * }),
684
+ * }
685
+ */
686
+ function beforeMiddleware(fn) {
687
+ return async ({ ctx, input, next }) => {
688
+ return next(await fn(ctx, input));
689
+ };
690
+ }
691
+ /**
692
+ * 组合多个 middleware 为一个
693
+ *
694
+ * @example
695
+ * middleware: {
696
+ * create: composeMiddleware(
697
+ * beforeMiddleware((ctx, input) => ({ ...input, slug: slugify(input.title) })),
698
+ * afterMiddleware((ctx, result) => sendEmail(result)),
699
+ * ),
700
+ * }
701
+ */
702
+ function composeMiddleware(...middlewares) {
703
+ return async (params) => {
704
+ let index = 0;
705
+ const executeNext = async (input) => {
706
+ if (index >= middlewares.length) return params.next(input);
707
+ const currentMiddleware = middlewares[index++];
708
+ return currentMiddleware({
709
+ ctx: params.ctx,
710
+ input,
711
+ next: executeNext
712
+ });
713
+ };
714
+ return executeNext(params.input);
715
+ };
716
+ }
717
+ /**
718
+ * 创建 list 操作的 after middleware
719
+ */
720
+ function afterList(fn) {
721
+ return afterMiddleware(fn);
722
+ }
723
+ /**
724
+ * 创建 list 操作的 before middleware
725
+ */
726
+ function beforeList(fn) {
727
+ return beforeMiddleware(fn);
728
+ }
729
+ /**
730
+ * 创建 create 操作的 after middleware
731
+ */
732
+ function afterCreate(fn) {
733
+ return afterMiddleware(fn);
734
+ }
735
+ /**
736
+ * 创建 create 操作的 before middleware
737
+ */
738
+ function beforeCreate(fn) {
739
+ return beforeMiddleware(fn);
740
+ }
741
+ /**
742
+ * 创建 update 操作的 after middleware
743
+ */
744
+ function afterUpdate(fn) {
745
+ return afterMiddleware(fn);
746
+ }
747
+ /**
748
+ * 创建 delete 操作的 after middleware
749
+ */
750
+ function afterDelete(fn) {
751
+ return afterMiddleware(fn);
752
+ }
753
+
619
754
  //#endregion
620
755
  //#region src/routers/index.ts
621
756
  const appRouter = router({});
622
757
 
623
758
  //#endregion
624
- export { appRouter, createCrudRouter, publicProcedure, router };
759
+ export { afterCreate, afterDelete, afterList, afterMiddleware, afterMiddlewareTransform, afterUpdate, appRouter, beforeCreate, beforeList, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
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.0",
4
+ "version": "0.6.0",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -47,8 +47,8 @@
47
47
  "typescript": "^5.9.3",
48
48
  "zod": "^4.1.13",
49
49
  "@internal/eslint-config": "0.3.0",
50
- "@internal/tsconfig": "0.1.0",
51
50
  "@internal/prettier-config": "0.0.1",
51
+ "@internal/tsconfig": "0.1.0",
52
52
  "@internal/tsdown-config": "0.1.0",
53
53
  "@internal/vitest-config": "0.1.0"
54
54
  },