@wordrhyme/auto-crud-server 0.3.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,9 +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 } = 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 m = middleware;
453
454
  const getIdColumn = () => table[idField];
454
455
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
455
456
  const buildWhere = (ctx, operation, additionalCondition) => {
@@ -477,50 +478,75 @@ function createCrudRouter(config) {
477
478
  const procedures = {
478
479
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
479
480
  await withGuard(ctx, "list");
480
- const offset = (input.page - 1) * input.perPage;
481
- const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
482
- const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
483
- table,
484
- filters: validatedFilters,
485
- joinOperator: input.joinOperator
486
- }) : void 0);
487
- let query = ctx.db.select().from(table).$dynamic();
488
- if (where) query = query.where(where);
489
- if (input.sort?.length) {
490
- const sortField = input.sort[0];
491
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
492
- const column = table[sortField.id];
493
- 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
+ }
494
497
  }
495
- }
496
- const data = await query.limit(input.perPage).offset(offset);
497
- let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
498
- if (where) countQuery = countQuery.where(where);
499
- const count = (await countQuery)[0]?.count ?? 0;
500
- return {
501
- data,
502
- total: count,
503
- page: input.page,
504
- perPage: input.perPage,
505
- pageCount: Math.ceil(count / input.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
+ };
506
509
  };
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);
507
517
  }),
508
518
  getById: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
509
519
  await withGuard(ctx, "get");
510
- const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
511
- const [item] = await ctx.db.select().from(table).where(where);
512
- await withAuthorize(ctx, item, "get");
513
- return item ?? null;
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();
514
532
  }),
515
533
  create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
516
534
  await withGuard(ctx, "create");
517
- const injectData = resolved.getInject(ctx, "create");
518
- const data = {
519
- ...input,
520
- ...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;
521
543
  };
522
- const [created] = await ctx.db.insert(table).values(data).returning();
523
- 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);
524
550
  }),
525
551
  update: resolved.procedureFactory("update").input(z.object({
526
552
  id: z.string(),
@@ -531,13 +557,23 @@ function createCrudRouter(config) {
531
557
  const [existing] = await ctx.db.select().from(table).where(where);
532
558
  await withAuthorize(ctx, existing, "update");
533
559
  if (!existing) throw new Error("Resource not found or access denied");
534
- const injectData = resolved.getInject(ctx, "update");
535
- const data = {
536
- ...input.data,
537
- ...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;
538
568
  };
539
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
540
- 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);
541
577
  }),
542
578
  delete: resolved.procedureFactory("delete").input(z.string()).mutation(async ({ ctx, input }) => {
543
579
  await withGuard(ctx, "delete");
@@ -545,18 +581,33 @@ function createCrudRouter(config) {
545
581
  const [existing] = await ctx.db.select().from(table).where(where);
546
582
  await withAuthorize(ctx, existing, "delete");
547
583
  if (!existing) throw new Error("Resource not found or access denied");
548
- if (softDelete) {
549
- const [deleted$1] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
550
- return deleted$1;
551
- }
552
- const [deleted] = await ctx.db.delete(table).where(where).returning();
553
- 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();
554
597
  }),
555
598
  deleteMany: resolved.procedureFactory("deleteMany").input(z.array(z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
556
599
  await withGuard(ctx, "deleteMany");
557
600
  const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
558
- if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
559
- return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
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();
560
611
  }),
561
612
  updateMany: resolved.procedureFactory("updateMany").input(z.object({
562
613
  ids: z.array(z.string()).max(maxBatchSize),
@@ -564,21 +615,145 @@ function createCrudRouter(config) {
564
615
  })).mutation(async ({ ctx, input }) => {
565
616
  await withGuard(ctx, "updateMany");
566
617
  const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
567
- const injectData = resolved.getInject(ctx, "update");
568
- const data = {
569
- ...input.data,
570
- ...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 };
571
625
  };
572
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
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);
573
633
  })
574
634
  };
575
635
  const crudRouter = router(procedures);
576
636
  return Object.assign(crudRouter, { procedures });
577
637
  }
578
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
+
579
754
  //#endregion
580
755
  //#region src/routers/index.ts
581
756
  const appRouter = router({});
582
757
 
583
758
  //#endregion
584
- 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.3.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",