@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/README.md +124 -1
- package/dist/index.cjs +240 -55
- package/dist/index.d.cts +294 -5
- package/dist/index.d.ts +294 -5
- package/dist/index.js +231 -56
- package/package.json +1 -1
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
559
|
-
|
|
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
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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