@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/README.md +124 -1
- package/dist/index.cjs +240 -95
- package/dist/index.d.cts +232 -72
- package/dist/index.d.ts +242 -82
- package/dist/index.js +231 -96
- package/package.json +2 -2
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,
|
|
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
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
table
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
"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
|
},
|