@wordrhyme/auto-crud-server 1.0.7 → 1.0.9
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.cjs +323 -49
- package/dist/index.d.cts +76 -8
- package/dist/index.d.ts +69 -1
- package/dist/index.js +324 -50
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
|
|
2
|
+
import { and, asc, desc, eq, getTableColumns, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
|
|
3
3
|
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
4
4
|
import { TRPCError, initTRPC } from "@trpc/server";
|
|
5
5
|
import superjson from "superjson";
|
|
@@ -422,12 +422,25 @@ function resolveSoftDelete(option) {
|
|
|
422
422
|
/**
|
|
423
423
|
* 判断 procedure 配置的类型
|
|
424
424
|
*/
|
|
425
|
+
function isProcedureLike(config) {
|
|
426
|
+
if (!config || typeof config !== "object") return false;
|
|
427
|
+
const candidate = config;
|
|
428
|
+
return [
|
|
429
|
+
"input",
|
|
430
|
+
"output",
|
|
431
|
+
"query",
|
|
432
|
+
"mutation",
|
|
433
|
+
"use"
|
|
434
|
+
].some((key) => typeof candidate[key] === "function");
|
|
435
|
+
}
|
|
425
436
|
function isProcedureMap(config) {
|
|
426
437
|
if (!config) return false;
|
|
427
438
|
if (typeof config === "function") return false;
|
|
439
|
+
if (isProcedureLike(config)) return false;
|
|
428
440
|
if (typeof config === "object" && config !== null) {
|
|
429
441
|
const keys = Object.keys(config);
|
|
430
442
|
const validKeys = [
|
|
443
|
+
"meta",
|
|
431
444
|
"list",
|
|
432
445
|
"get",
|
|
433
446
|
"create",
|
|
@@ -473,6 +486,9 @@ function resolveConfig(config) {
|
|
|
473
486
|
}
|
|
474
487
|
};
|
|
475
488
|
}
|
|
489
|
+
function resolveMetaProcedure(config, fallback) {
|
|
490
|
+
return isProcedureMap(config) ? config.meta ?? fallback : fallback;
|
|
491
|
+
}
|
|
476
492
|
const filterItemSchema = z.object({
|
|
477
493
|
id: z.string(),
|
|
478
494
|
value: z.union([z.string(), z.array(z.string())]),
|
|
@@ -487,6 +503,7 @@ const baseListInputSchema = z.object({
|
|
|
487
503
|
id: z.string(),
|
|
488
504
|
desc: z.boolean()
|
|
489
505
|
})).optional(),
|
|
506
|
+
search: z.string().trim().optional(),
|
|
490
507
|
filters: z.array(filterItemSchema).optional(),
|
|
491
508
|
joinOperator: z.enum(["and", "or"]).default("and")
|
|
492
509
|
});
|
|
@@ -497,6 +514,7 @@ const baseExportInputSchema = z.object({
|
|
|
497
514
|
id: z.string(),
|
|
498
515
|
desc: z.boolean()
|
|
499
516
|
})).optional(),
|
|
517
|
+
search: z.string().trim().optional(),
|
|
500
518
|
filters: z.array(filterItemSchema).optional(),
|
|
501
519
|
joinOperator: z.enum(["and", "or"]).default("and"),
|
|
502
520
|
limit: z.number().min(1).optional()
|
|
@@ -514,6 +532,13 @@ const DEFAULT_OMIT_FIELDS = [
|
|
|
514
532
|
"createdAt",
|
|
515
533
|
"updatedAt"
|
|
516
534
|
];
|
|
535
|
+
function shouldEnableCrudExtensionSchema(config) {
|
|
536
|
+
return Boolean(config.id) && config.extensions !== false;
|
|
537
|
+
}
|
|
538
|
+
function withCrudExtensionInput(schema, config) {
|
|
539
|
+
if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
|
|
540
|
+
return schema.passthrough();
|
|
541
|
+
}
|
|
517
542
|
/**
|
|
518
543
|
* 解析并派生 Schema
|
|
519
544
|
* - 优先使用显式传入的 Schema
|
|
@@ -531,11 +556,12 @@ function resolveSchemas(config) {
|
|
|
531
556
|
const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
|
|
532
557
|
schema = rawSchema.omit(omitConfig);
|
|
533
558
|
}
|
|
559
|
+
schema = withCrudExtensionInput(schema, config);
|
|
534
560
|
let selectSchema;
|
|
535
561
|
if (config.selectSchema) selectSchema = config.selectSchema;
|
|
536
562
|
else if (config.schema) selectSchema = schema;
|
|
537
563
|
else selectSchema = createSelectSchema(table);
|
|
538
|
-
const updateSchema = config.updateSchema
|
|
564
|
+
const updateSchema = config.updateSchema ? withCrudExtensionInput(config.updateSchema, config) : schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
|
|
539
565
|
return {
|
|
540
566
|
selectSchema,
|
|
541
567
|
schema,
|
|
@@ -580,6 +606,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
580
606
|
}
|
|
581
607
|
return getTableColumn(table, columnId);
|
|
582
608
|
}
|
|
609
|
+
const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
|
|
610
|
+
const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
|
|
611
|
+
function isObjectRecord(value) {
|
|
612
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
613
|
+
}
|
|
614
|
+
function isCrudExtensionIdFilter(filter, idField) {
|
|
615
|
+
return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
|
|
616
|
+
}
|
|
617
|
+
function readCrudTarget(config) {
|
|
618
|
+
const id = config.id;
|
|
619
|
+
if (!id) return null;
|
|
620
|
+
return { id };
|
|
621
|
+
}
|
|
622
|
+
function resolveCrudExtensions(ctx, config) {
|
|
623
|
+
const extensions = config.extensions;
|
|
624
|
+
if (extensions === false) return null;
|
|
625
|
+
if (typeof extensions === "function") return extensions(ctx) ?? null;
|
|
626
|
+
if (extensions) return extensions;
|
|
627
|
+
if (!config.id) return null;
|
|
628
|
+
return ctx?.crudExtensions ?? null;
|
|
629
|
+
}
|
|
630
|
+
function getTableColumnNames(table) {
|
|
631
|
+
try {
|
|
632
|
+
const columns = getTableColumns(table);
|
|
633
|
+
const names = Object.keys(columns);
|
|
634
|
+
if (names.length > 0) return new Set(names);
|
|
635
|
+
} catch {}
|
|
636
|
+
return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
|
|
637
|
+
}
|
|
638
|
+
function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
639
|
+
if (!isObjectRecord(inputData)) return {
|
|
640
|
+
data: inputData,
|
|
641
|
+
rawValues: {},
|
|
642
|
+
baseValues: {},
|
|
643
|
+
extraValues: null
|
|
644
|
+
};
|
|
645
|
+
const baseValues = {};
|
|
646
|
+
const extraValues = {};
|
|
647
|
+
for (const [key, value] of Object.entries(inputData)) {
|
|
648
|
+
if (key === "ext") {
|
|
649
|
+
if (isObjectRecord(value)) Object.assign(extraValues, value);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (tableColumnNames.has(key)) baseValues[key] = value;
|
|
653
|
+
else extraValues[key] = value;
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
data: baseValues,
|
|
657
|
+
rawValues: inputData,
|
|
658
|
+
baseValues,
|
|
659
|
+
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
663
|
+
const target = readCrudTarget(config);
|
|
664
|
+
const extraValues = input.extraValues;
|
|
665
|
+
if (!target || !extraValues) return;
|
|
666
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
667
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
668
|
+
if (!provider?.saveExtraValues) throw new TRPCError({
|
|
669
|
+
code: "PRECONDITION_FAILED",
|
|
670
|
+
message: "CRUD extension persistence is not available"
|
|
671
|
+
});
|
|
672
|
+
await provider.saveExtraValues({
|
|
673
|
+
id: target.id,
|
|
674
|
+
entityId: String(entityId),
|
|
675
|
+
rawValues: input.rawValues,
|
|
676
|
+
baseValues: input.baseValues,
|
|
677
|
+
extraValues,
|
|
678
|
+
tx: ctx?.tx
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
682
|
+
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
683
|
+
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new TRPCError({
|
|
684
|
+
code: "PRECONDITION_FAILED",
|
|
685
|
+
message: "CRUD extension persistence is not available"
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
689
|
+
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
690
|
+
}
|
|
691
|
+
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
692
|
+
const extraByInputId = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const input of inputs) {
|
|
694
|
+
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
695
|
+
const inputId = input.data[idField];
|
|
696
|
+
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
697
|
+
}
|
|
698
|
+
const canUseIndexFallback = results.length === inputs.length;
|
|
699
|
+
await Promise.all(results.map((result, index) => {
|
|
700
|
+
const resultId = result[idField];
|
|
701
|
+
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
702
|
+
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
703
|
+
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
704
|
+
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
705
|
+
return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
function readProjectionDisplay(value) {
|
|
709
|
+
if (!isObjectRecord(value)) return value;
|
|
710
|
+
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
711
|
+
if ("value" in value) return value.value;
|
|
712
|
+
return value;
|
|
713
|
+
}
|
|
714
|
+
async function enrichCrudRows(ctx, config, idField, rows) {
|
|
715
|
+
const target = readCrudTarget(config);
|
|
716
|
+
if (!target || rows.length === 0) return rows;
|
|
717
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
718
|
+
if (!provider?.readProjection) return rows;
|
|
719
|
+
const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
|
|
720
|
+
if (entityIds.length === 0) return rows;
|
|
721
|
+
const projections = await provider.readProjection({
|
|
722
|
+
id: target.id,
|
|
723
|
+
entityIds
|
|
724
|
+
});
|
|
725
|
+
return rows.map((row) => {
|
|
726
|
+
const rowId = row[idField];
|
|
727
|
+
const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
|
|
728
|
+
if (!projected || Object.keys(projected).length === 0) return row;
|
|
729
|
+
return {
|
|
730
|
+
...row,
|
|
731
|
+
...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
736
|
+
if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
|
|
737
|
+
table,
|
|
738
|
+
columnId,
|
|
739
|
+
allowedColumns
|
|
740
|
+
}) !== void 0;
|
|
741
|
+
return getTableColumn(table, columnId) !== void 0;
|
|
742
|
+
}
|
|
743
|
+
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
744
|
+
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
745
|
+
}
|
|
746
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
747
|
+
const target = readCrudTarget(config);
|
|
748
|
+
const filters = input.filters ?? [];
|
|
749
|
+
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
750
|
+
if (!target || filters.length === 0 && !search) return input;
|
|
751
|
+
const baseFilters = [];
|
|
752
|
+
const extensionFilters = [];
|
|
753
|
+
for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
754
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
755
|
+
else extensionFilters.push(filter);
|
|
756
|
+
if (extensionFilters.length === 0 && !search) return {
|
|
757
|
+
...input,
|
|
758
|
+
filters: baseFilters
|
|
759
|
+
};
|
|
760
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
761
|
+
const matchedSets = [];
|
|
762
|
+
if (extensionFilters.length > 0) {
|
|
763
|
+
if (!provider?.matchEntityIds) throw new TRPCError({
|
|
764
|
+
code: "PRECONDITION_FAILED",
|
|
765
|
+
message: "CRUD extension filtering is not available"
|
|
766
|
+
});
|
|
767
|
+
const matchedIds$1 = await provider.matchEntityIds({
|
|
768
|
+
id: target.id,
|
|
769
|
+
filters: extensionFilters,
|
|
770
|
+
joinOperator: input.joinOperator,
|
|
771
|
+
limit: 5e3
|
|
772
|
+
});
|
|
773
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
774
|
+
}
|
|
775
|
+
if (search && provider?.searchEntityIds) {
|
|
776
|
+
const matchedIds$1 = await provider.searchEntityIds({
|
|
777
|
+
id: target.id,
|
|
778
|
+
search,
|
|
779
|
+
limit: 5e3
|
|
780
|
+
});
|
|
781
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
782
|
+
}
|
|
783
|
+
if (matchedSets.length === 0) return {
|
|
784
|
+
...input,
|
|
785
|
+
filters: baseFilters
|
|
786
|
+
};
|
|
787
|
+
let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
|
|
788
|
+
for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
|
|
789
|
+
return {
|
|
790
|
+
...input,
|
|
791
|
+
filters: [...baseFilters, {
|
|
792
|
+
id: idField,
|
|
793
|
+
value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
|
|
794
|
+
variant: "multiSelect",
|
|
795
|
+
operator: "inArray",
|
|
796
|
+
filterId: CRUD_EXTENSION_FILTER_ID
|
|
797
|
+
}]
|
|
798
|
+
};
|
|
799
|
+
}
|
|
583
800
|
/**
|
|
584
801
|
* 创建 CRUD Router
|
|
585
802
|
*
|
|
@@ -595,14 +812,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
595
812
|
function createCrudRouter(config) {
|
|
596
813
|
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
597
814
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
598
|
-
const entityOutputSchema = selectSchema;
|
|
815
|
+
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
599
816
|
const listOutputSchema = z.object({
|
|
600
|
-
data: z.array(
|
|
817
|
+
data: z.array(entityOutputSchema),
|
|
601
818
|
total: z.number(),
|
|
602
819
|
page: z.number(),
|
|
603
820
|
perPage: z.number(),
|
|
604
821
|
pageCount: z.number()
|
|
605
822
|
});
|
|
823
|
+
const metadataOutputSchema = z.object({
|
|
824
|
+
schema: z.unknown().optional(),
|
|
825
|
+
fields: z.record(z.string(), z.unknown()).optional(),
|
|
826
|
+
errors: z.array(z.string()).optional()
|
|
827
|
+
});
|
|
606
828
|
const deleteManyOutputSchema = z.object({ deleted: z.number() });
|
|
607
829
|
const updateManyOutputSchema = z.object({ updated: z.number() });
|
|
608
830
|
const upsertOutputSchema = z.object({
|
|
@@ -610,12 +832,12 @@ function createCrudRouter(config) {
|
|
|
610
832
|
isNew: z.boolean()
|
|
611
833
|
});
|
|
612
834
|
const exportOutputSchema = z.object({
|
|
613
|
-
data: z.array(
|
|
835
|
+
data: z.array(entityOutputSchema),
|
|
614
836
|
total: z.number(),
|
|
615
837
|
hasMore: z.boolean()
|
|
616
838
|
});
|
|
617
839
|
const createManyOutputSchema = z.object({
|
|
618
|
-
created: z.array(
|
|
840
|
+
created: z.array(entityOutputSchema),
|
|
619
841
|
count: z.number()
|
|
620
842
|
});
|
|
621
843
|
const importOutputSchema = z.object({
|
|
@@ -628,12 +850,14 @@ function createCrudRouter(config) {
|
|
|
628
850
|
}))
|
|
629
851
|
});
|
|
630
852
|
const resolved = resolveConfig(config);
|
|
853
|
+
const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
|
|
631
854
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
632
855
|
const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
|
|
633
856
|
const resolvedGetInputSchema = config.getInputSchema ? z.union([z.string(), config.getInputSchema]) : defaultGetInputSchema;
|
|
634
857
|
const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
|
|
635
858
|
const m = middleware;
|
|
636
859
|
const getIdColumn = () => table[idField];
|
|
860
|
+
const tableColumnNames = getTableColumnNames(table);
|
|
637
861
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
638
862
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
639
863
|
const conditions = [];
|
|
@@ -673,25 +897,38 @@ function createCrudRouter(config) {
|
|
|
673
897
|
});
|
|
674
898
|
};
|
|
675
899
|
const procedures = {
|
|
900
|
+
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
901
|
+
await withGuard(ctx, "list");
|
|
902
|
+
const target = readCrudTarget(config);
|
|
903
|
+
if (!target) return {};
|
|
904
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
905
|
+
if (!provider?.getMetadata) return {};
|
|
906
|
+
return provider.getMetadata({ id: target.id });
|
|
907
|
+
}),
|
|
676
908
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
677
909
|
await withGuard(ctx, "list");
|
|
678
910
|
const doList = async (listInput$1) => {
|
|
679
|
-
const
|
|
680
|
-
const
|
|
911
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
912
|
+
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
913
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
681
914
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
682
915
|
table,
|
|
683
916
|
filters: validatedFilters,
|
|
684
|
-
joinOperator:
|
|
685
|
-
resolveColumn: (columnId) =>
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
917
|
+
joinOperator: effectiveInput.joinOperator,
|
|
918
|
+
resolveColumn: (columnId) => {
|
|
919
|
+
const id = String(columnId);
|
|
920
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
921
|
+
return resolveColumnTarget({
|
|
922
|
+
table,
|
|
923
|
+
columnId: id,
|
|
924
|
+
allowedColumns: filterableColumns
|
|
925
|
+
});
|
|
926
|
+
}
|
|
690
927
|
}) : void 0);
|
|
691
928
|
let query = ctx.db.select().from(table).$dynamic();
|
|
692
929
|
if (where) query = query.where(where);
|
|
693
|
-
if (
|
|
694
|
-
const sortField =
|
|
930
|
+
if (effectiveInput.sort?.length) {
|
|
931
|
+
const sortField = effectiveInput.sort[0];
|
|
695
932
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
696
933
|
const column = resolveColumnTarget({
|
|
697
934
|
table,
|
|
@@ -701,16 +938,16 @@ function createCrudRouter(config) {
|
|
|
701
938
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
702
939
|
}
|
|
703
940
|
}
|
|
704
|
-
const
|
|
941
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
705
942
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
706
943
|
if (where) countQuery = countQuery.where(where);
|
|
707
944
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
708
945
|
return {
|
|
709
|
-
data,
|
|
946
|
+
data: enrichedData,
|
|
710
947
|
total: count,
|
|
711
|
-
page:
|
|
712
|
-
perPage:
|
|
713
|
-
pageCount: Math.ceil(count /
|
|
948
|
+
page: effectiveInput.page,
|
|
949
|
+
perPage: effectiveInput.perPage,
|
|
950
|
+
pageCount: Math.ceil(count / effectiveInput.perPage)
|
|
714
951
|
};
|
|
715
952
|
};
|
|
716
953
|
const listInput = input;
|
|
@@ -728,7 +965,9 @@ function createCrudRouter(config) {
|
|
|
728
965
|
const where = buildWhere(ctx, "get", eq(getIdColumn(), id$1));
|
|
729
966
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
730
967
|
await withAuthorize(ctx, item, "get");
|
|
731
|
-
|
|
968
|
+
if (!item) return null;
|
|
969
|
+
const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
|
|
970
|
+
return enriched ?? null;
|
|
732
971
|
};
|
|
733
972
|
const id = typeof input === "string" ? input : input.id;
|
|
734
973
|
if (m.get) return m.get({
|
|
@@ -742,12 +981,15 @@ function createCrudRouter(config) {
|
|
|
742
981
|
create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
|
|
743
982
|
await withGuard(ctx, "create");
|
|
744
983
|
const doCreate = async (inputData) => {
|
|
984
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
985
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
745
986
|
const injectData = resolved.getInject(ctx, "create");
|
|
746
987
|
const data = {
|
|
747
|
-
...
|
|
988
|
+
...splitInput.data,
|
|
748
989
|
...injectData
|
|
749
990
|
};
|
|
750
991
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
992
|
+
await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
|
|
751
993
|
return created;
|
|
752
994
|
};
|
|
753
995
|
if (m.create) return m.create({
|
|
@@ -770,12 +1012,16 @@ function createCrudRouter(config) {
|
|
|
770
1012
|
message: "Resource not found or access denied"
|
|
771
1013
|
});
|
|
772
1014
|
const doUpdate = async (updateData) => {
|
|
1015
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1016
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
773
1017
|
const injectData = resolved.getInject(ctx, "update");
|
|
774
1018
|
const data = {
|
|
775
|
-
...
|
|
1019
|
+
...splitUpdate.data,
|
|
776
1020
|
...injectData
|
|
777
1021
|
};
|
|
778
|
-
|
|
1022
|
+
let updated = existing;
|
|
1023
|
+
if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
1024
|
+
await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
|
|
779
1025
|
return updated;
|
|
780
1026
|
};
|
|
781
1027
|
if (m.update) return m.update({
|
|
@@ -831,12 +1077,18 @@ function createCrudRouter(config) {
|
|
|
831
1077
|
await withGuard(ctx, "updateMany");
|
|
832
1078
|
const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
|
|
833
1079
|
const doUpdateMany = async (updateData) => {
|
|
1080
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1081
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
834
1082
|
const injectData = resolved.getInject(ctx, "update");
|
|
835
1083
|
const data = {
|
|
836
|
-
...
|
|
1084
|
+
...splitUpdate.data,
|
|
837
1085
|
...injectData
|
|
838
1086
|
};
|
|
839
|
-
|
|
1087
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1088
|
+
if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1089
|
+
else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1090
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
|
|
1091
|
+
return { updated: updated.length };
|
|
840
1092
|
};
|
|
841
1093
|
if (m.updateMany) return m.updateMany({
|
|
842
1094
|
ctx,
|
|
@@ -849,7 +1101,9 @@ function createCrudRouter(config) {
|
|
|
849
1101
|
upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
|
|
850
1102
|
await withGuard(ctx, "upsert");
|
|
851
1103
|
const doUpsert = async (inputData) => {
|
|
852
|
-
const
|
|
1104
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1105
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
1106
|
+
const inputId = splitInput.data[idField];
|
|
853
1107
|
let isNew = true;
|
|
854
1108
|
let existing = null;
|
|
855
1109
|
if (inputId) {
|
|
@@ -861,16 +1115,17 @@ function createCrudRouter(config) {
|
|
|
861
1115
|
}
|
|
862
1116
|
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
863
1117
|
const data = {
|
|
864
|
-
...
|
|
1118
|
+
...splitInput.data,
|
|
865
1119
|
...injectData
|
|
866
1120
|
};
|
|
867
1121
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
868
1122
|
target: getIdColumn(),
|
|
869
1123
|
set: {
|
|
870
|
-
...
|
|
1124
|
+
...splitInput.data,
|
|
871
1125
|
...resolved.getInject(ctx, "update")
|
|
872
1126
|
}
|
|
873
1127
|
}).returning();
|
|
1128
|
+
await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
|
|
874
1129
|
return {
|
|
875
1130
|
data: result,
|
|
876
1131
|
isNew
|
|
@@ -886,22 +1141,32 @@ function createCrudRouter(config) {
|
|
|
886
1141
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
887
1142
|
await withGuard(ctx, "export");
|
|
888
1143
|
const doExport = async (exportInput$1) => {
|
|
889
|
-
const
|
|
890
|
-
|
|
1144
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1145
|
+
page: 1,
|
|
1146
|
+
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1147
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
1148
|
+
...exportInput$1
|
|
1149
|
+
});
|
|
1150
|
+
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1151
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
891
1152
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
892
1153
|
table,
|
|
893
1154
|
filters: validatedFilters,
|
|
894
|
-
joinOperator:
|
|
895
|
-
resolveColumn: (columnId) =>
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1155
|
+
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
1156
|
+
resolveColumn: (columnId) => {
|
|
1157
|
+
const id = String(columnId);
|
|
1158
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
1159
|
+
return resolveColumnTarget({
|
|
1160
|
+
table,
|
|
1161
|
+
columnId: id,
|
|
1162
|
+
allowedColumns: filterableColumns
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
900
1165
|
}) : void 0);
|
|
901
1166
|
let query = ctx.db.select().from(table).$dynamic();
|
|
902
1167
|
if (where) query = query.where(where);
|
|
903
|
-
if (
|
|
904
|
-
const sortField =
|
|
1168
|
+
if (effectiveInput.sort?.length) {
|
|
1169
|
+
const sortField = effectiveInput.sort[0];
|
|
905
1170
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
906
1171
|
const column = resolveColumnTarget({
|
|
907
1172
|
table,
|
|
@@ -911,14 +1176,14 @@ function createCrudRouter(config) {
|
|
|
911
1176
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
912
1177
|
}
|
|
913
1178
|
}
|
|
914
|
-
const
|
|
1179
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
915
1180
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
916
1181
|
if (where) countQuery = countQuery.where(where);
|
|
917
1182
|
const total = (await countQuery)[0]?.count ?? 0;
|
|
918
1183
|
return {
|
|
919
|
-
data,
|
|
1184
|
+
data: enrichedData,
|
|
920
1185
|
total,
|
|
921
|
-
hasMore: total >
|
|
1186
|
+
hasMore: total > enrichedData.length
|
|
922
1187
|
};
|
|
923
1188
|
};
|
|
924
1189
|
const exportInput = input;
|
|
@@ -932,12 +1197,15 @@ function createCrudRouter(config) {
|
|
|
932
1197
|
createMany: resolved.procedureFactory("createMany").input(z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
|
|
933
1198
|
await withGuard(ctx, "createMany");
|
|
934
1199
|
const doCreateMany = async (items) => {
|
|
1200
|
+
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1201
|
+
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
935
1202
|
const injectData = resolved.getInject(ctx, "create");
|
|
936
|
-
const values =
|
|
937
|
-
...
|
|
1203
|
+
const values = splitItems.map(({ data }) => ({
|
|
1204
|
+
...data,
|
|
938
1205
|
...injectData
|
|
939
1206
|
}));
|
|
940
1207
|
const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1208
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
|
|
941
1209
|
return {
|
|
942
1210
|
created,
|
|
943
1211
|
count: created.length
|
|
@@ -958,8 +1226,10 @@ function createCrudRouter(config) {
|
|
|
958
1226
|
for (let i = 0; i < importData$1.rows.length; i++) {
|
|
959
1227
|
const row = importData$1.rows[i];
|
|
960
1228
|
const result = schema.safeParse(row);
|
|
961
|
-
if (result.success)
|
|
962
|
-
|
|
1229
|
+
if (result.success) {
|
|
1230
|
+
const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
|
|
1231
|
+
validRows.push(splitRow);
|
|
1232
|
+
} else {
|
|
963
1233
|
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
964
1234
|
failed.push({
|
|
965
1235
|
row: i,
|
|
@@ -979,26 +1249,30 @@ function createCrudRouter(config) {
|
|
|
979
1249
|
skipped: 0,
|
|
980
1250
|
failed
|
|
981
1251
|
};
|
|
1252
|
+
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
982
1253
|
const injectData = resolved.getInject(ctx, "create");
|
|
983
1254
|
const values = validRows.map((row) => ({
|
|
984
|
-
...row,
|
|
1255
|
+
...row.data,
|
|
985
1256
|
...injectData
|
|
986
1257
|
}));
|
|
987
1258
|
let insertedCount = 0;
|
|
988
1259
|
let updatedCount = 0;
|
|
989
1260
|
if (importData$1.onConflict === "upsert") {
|
|
990
1261
|
const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where(inArray(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
|
|
991
|
-
const updateFields = Object.keys(validRows[0]);
|
|
1262
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
992
1263
|
const setClause = { ...resolved.getInject(ctx, "update") };
|
|
993
1264
|
for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
|
|
994
1265
|
const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
|
|
995
1266
|
target: getIdColumn(),
|
|
996
1267
|
set: setClause
|
|
997
1268
|
}).returning({ id: getIdColumn() });
|
|
1269
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
998
1270
|
updatedCount = Math.min(existingCount, results.length);
|
|
999
1271
|
insertedCount = results.length - updatedCount;
|
|
1000
1272
|
} else {
|
|
1001
|
-
|
|
1273
|
+
const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1274
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1275
|
+
insertedCount = inserted.length;
|
|
1002
1276
|
updatedCount = 0;
|
|
1003
1277
|
}
|
|
1004
1278
|
const skipped = validRows.length - insertedCount - updatedCount;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.9",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -38,20 +38,20 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@trpc/server": "^11.0.0",
|
|
41
|
-
"@types/node": "^22.19.
|
|
41
|
+
"@types/node": "^22.19.17",
|
|
42
42
|
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
|
43
43
|
"drizzle-zod": "1.0.0-beta.14-a36c63d",
|
|
44
|
-
"eslint": "^9.39.
|
|
44
|
+
"eslint": "^9.39.4",
|
|
45
45
|
"superjson": "^2.2.6",
|
|
46
46
|
"tsdown": "^0.15.12",
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
48
|
"vitest": "^4.0.18",
|
|
49
|
-
"zod": "^4.
|
|
49
|
+
"zod": "^4.3.6",
|
|
50
50
|
"@internal/eslint-config": "0.3.0",
|
|
51
|
-
"@internal/tsconfig": "0.1.0",
|
|
52
51
|
"@internal/prettier-config": "0.0.1",
|
|
53
52
|
"@internal/tsdown-config": "0.1.0",
|
|
54
|
-
"@internal/vitest-config": "0.1.0"
|
|
53
|
+
"@internal/vitest-config": "0.1.0",
|
|
54
|
+
"@internal/tsconfig": "0.1.0"
|
|
55
55
|
},
|
|
56
56
|
"prettier": "@internal/prettier-config",
|
|
57
57
|
"scripts": {
|