@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.cjs
CHANGED
|
@@ -451,12 +451,25 @@ function resolveSoftDelete(option) {
|
|
|
451
451
|
/**
|
|
452
452
|
* 判断 procedure 配置的类型
|
|
453
453
|
*/
|
|
454
|
+
function isProcedureLike(config) {
|
|
455
|
+
if (!config || typeof config !== "object") return false;
|
|
456
|
+
const candidate = config;
|
|
457
|
+
return [
|
|
458
|
+
"input",
|
|
459
|
+
"output",
|
|
460
|
+
"query",
|
|
461
|
+
"mutation",
|
|
462
|
+
"use"
|
|
463
|
+
].some((key) => typeof candidate[key] === "function");
|
|
464
|
+
}
|
|
454
465
|
function isProcedureMap(config) {
|
|
455
466
|
if (!config) return false;
|
|
456
467
|
if (typeof config === "function") return false;
|
|
468
|
+
if (isProcedureLike(config)) return false;
|
|
457
469
|
if (typeof config === "object" && config !== null) {
|
|
458
470
|
const keys = Object.keys(config);
|
|
459
471
|
const validKeys = [
|
|
472
|
+
"meta",
|
|
460
473
|
"list",
|
|
461
474
|
"get",
|
|
462
475
|
"create",
|
|
@@ -502,6 +515,9 @@ function resolveConfig(config) {
|
|
|
502
515
|
}
|
|
503
516
|
};
|
|
504
517
|
}
|
|
518
|
+
function resolveMetaProcedure(config, fallback) {
|
|
519
|
+
return isProcedureMap(config) ? config.meta ?? fallback : fallback;
|
|
520
|
+
}
|
|
505
521
|
const filterItemSchema = zod.z.object({
|
|
506
522
|
id: zod.z.string(),
|
|
507
523
|
value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
|
|
@@ -516,6 +532,7 @@ const baseListInputSchema = zod.z.object({
|
|
|
516
532
|
id: zod.z.string(),
|
|
517
533
|
desc: zod.z.boolean()
|
|
518
534
|
})).optional(),
|
|
535
|
+
search: zod.z.string().trim().optional(),
|
|
519
536
|
filters: zod.z.array(filterItemSchema).optional(),
|
|
520
537
|
joinOperator: zod.z.enum(["and", "or"]).default("and")
|
|
521
538
|
});
|
|
@@ -526,6 +543,7 @@ const baseExportInputSchema = zod.z.object({
|
|
|
526
543
|
id: zod.z.string(),
|
|
527
544
|
desc: zod.z.boolean()
|
|
528
545
|
})).optional(),
|
|
546
|
+
search: zod.z.string().trim().optional(),
|
|
529
547
|
filters: zod.z.array(filterItemSchema).optional(),
|
|
530
548
|
joinOperator: zod.z.enum(["and", "or"]).default("and"),
|
|
531
549
|
limit: zod.z.number().min(1).optional()
|
|
@@ -543,6 +561,13 @@ const DEFAULT_OMIT_FIELDS = [
|
|
|
543
561
|
"createdAt",
|
|
544
562
|
"updatedAt"
|
|
545
563
|
];
|
|
564
|
+
function shouldEnableCrudExtensionSchema(config) {
|
|
565
|
+
return Boolean(config.id) && config.extensions !== false;
|
|
566
|
+
}
|
|
567
|
+
function withCrudExtensionInput(schema, config) {
|
|
568
|
+
if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
|
|
569
|
+
return schema.passthrough();
|
|
570
|
+
}
|
|
546
571
|
/**
|
|
547
572
|
* 解析并派生 Schema
|
|
548
573
|
* - 优先使用显式传入的 Schema
|
|
@@ -560,11 +585,12 @@ function resolveSchemas(config) {
|
|
|
560
585
|
const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
|
|
561
586
|
schema = rawSchema.omit(omitConfig);
|
|
562
587
|
}
|
|
588
|
+
schema = withCrudExtensionInput(schema, config);
|
|
563
589
|
let selectSchema;
|
|
564
590
|
if (config.selectSchema) selectSchema = config.selectSchema;
|
|
565
591
|
else if (config.schema) selectSchema = schema;
|
|
566
592
|
else selectSchema = (0, drizzle_zod.createSelectSchema)(table);
|
|
567
|
-
const updateSchema = config.updateSchema
|
|
593
|
+
const updateSchema = config.updateSchema ? withCrudExtensionInput(config.updateSchema, config) : schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
|
|
568
594
|
return {
|
|
569
595
|
selectSchema,
|
|
570
596
|
schema,
|
|
@@ -609,6 +635,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
609
635
|
}
|
|
610
636
|
return getTableColumn(table, columnId);
|
|
611
637
|
}
|
|
638
|
+
const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
|
|
639
|
+
const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
|
|
640
|
+
function isObjectRecord(value) {
|
|
641
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
642
|
+
}
|
|
643
|
+
function isCrudExtensionIdFilter(filter, idField) {
|
|
644
|
+
return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
|
|
645
|
+
}
|
|
646
|
+
function readCrudTarget(config) {
|
|
647
|
+
const id = config.id;
|
|
648
|
+
if (!id) return null;
|
|
649
|
+
return { id };
|
|
650
|
+
}
|
|
651
|
+
function resolveCrudExtensions(ctx, config) {
|
|
652
|
+
const extensions = config.extensions;
|
|
653
|
+
if (extensions === false) return null;
|
|
654
|
+
if (typeof extensions === "function") return extensions(ctx) ?? null;
|
|
655
|
+
if (extensions) return extensions;
|
|
656
|
+
if (!config.id) return null;
|
|
657
|
+
return ctx?.crudExtensions ?? null;
|
|
658
|
+
}
|
|
659
|
+
function getTableColumnNames(table) {
|
|
660
|
+
try {
|
|
661
|
+
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
662
|
+
const names = Object.keys(columns);
|
|
663
|
+
if (names.length > 0) return new Set(names);
|
|
664
|
+
} catch {}
|
|
665
|
+
return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
|
|
666
|
+
}
|
|
667
|
+
function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
668
|
+
if (!isObjectRecord(inputData)) return {
|
|
669
|
+
data: inputData,
|
|
670
|
+
rawValues: {},
|
|
671
|
+
baseValues: {},
|
|
672
|
+
extraValues: null
|
|
673
|
+
};
|
|
674
|
+
const baseValues = {};
|
|
675
|
+
const extraValues = {};
|
|
676
|
+
for (const [key, value] of Object.entries(inputData)) {
|
|
677
|
+
if (key === "ext") {
|
|
678
|
+
if (isObjectRecord(value)) Object.assign(extraValues, value);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (tableColumnNames.has(key)) baseValues[key] = value;
|
|
682
|
+
else extraValues[key] = value;
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
data: baseValues,
|
|
686
|
+
rawValues: inputData,
|
|
687
|
+
baseValues,
|
|
688
|
+
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
692
|
+
const target = readCrudTarget(config);
|
|
693
|
+
const extraValues = input.extraValues;
|
|
694
|
+
if (!target || !extraValues) return;
|
|
695
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
696
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
697
|
+
if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
698
|
+
code: "PRECONDITION_FAILED",
|
|
699
|
+
message: "CRUD extension persistence is not available"
|
|
700
|
+
});
|
|
701
|
+
await provider.saveExtraValues({
|
|
702
|
+
id: target.id,
|
|
703
|
+
entityId: String(entityId),
|
|
704
|
+
rawValues: input.rawValues,
|
|
705
|
+
baseValues: input.baseValues,
|
|
706
|
+
extraValues,
|
|
707
|
+
tx: ctx?.tx
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
711
|
+
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
712
|
+
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
713
|
+
code: "PRECONDITION_FAILED",
|
|
714
|
+
message: "CRUD extension persistence is not available"
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
718
|
+
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
719
|
+
}
|
|
720
|
+
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
721
|
+
const extraByInputId = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const input of inputs) {
|
|
723
|
+
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
724
|
+
const inputId = input.data[idField];
|
|
725
|
+
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
726
|
+
}
|
|
727
|
+
const canUseIndexFallback = results.length === inputs.length;
|
|
728
|
+
await Promise.all(results.map((result, index) => {
|
|
729
|
+
const resultId = result[idField];
|
|
730
|
+
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
731
|
+
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
732
|
+
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
733
|
+
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
734
|
+
return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
|
|
735
|
+
}));
|
|
736
|
+
}
|
|
737
|
+
function readProjectionDisplay(value) {
|
|
738
|
+
if (!isObjectRecord(value)) return value;
|
|
739
|
+
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
740
|
+
if ("value" in value) return value.value;
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
async function enrichCrudRows(ctx, config, idField, rows) {
|
|
744
|
+
const target = readCrudTarget(config);
|
|
745
|
+
if (!target || rows.length === 0) return rows;
|
|
746
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
747
|
+
if (!provider?.readProjection) return rows;
|
|
748
|
+
const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
|
|
749
|
+
if (entityIds.length === 0) return rows;
|
|
750
|
+
const projections = await provider.readProjection({
|
|
751
|
+
id: target.id,
|
|
752
|
+
entityIds
|
|
753
|
+
});
|
|
754
|
+
return rows.map((row) => {
|
|
755
|
+
const rowId = row[idField];
|
|
756
|
+
const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
|
|
757
|
+
if (!projected || Object.keys(projected).length === 0) return row;
|
|
758
|
+
return {
|
|
759
|
+
...row,
|
|
760
|
+
...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
765
|
+
if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
|
|
766
|
+
table,
|
|
767
|
+
columnId,
|
|
768
|
+
allowedColumns
|
|
769
|
+
}) !== void 0;
|
|
770
|
+
return getTableColumn(table, columnId) !== void 0;
|
|
771
|
+
}
|
|
772
|
+
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
773
|
+
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
774
|
+
}
|
|
775
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
776
|
+
const target = readCrudTarget(config);
|
|
777
|
+
const filters = input.filters ?? [];
|
|
778
|
+
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
779
|
+
if (!target || filters.length === 0 && !search) return input;
|
|
780
|
+
const baseFilters = [];
|
|
781
|
+
const extensionFilters = [];
|
|
782
|
+
for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
783
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
784
|
+
else extensionFilters.push(filter);
|
|
785
|
+
if (extensionFilters.length === 0 && !search) return {
|
|
786
|
+
...input,
|
|
787
|
+
filters: baseFilters
|
|
788
|
+
};
|
|
789
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
790
|
+
const matchedSets = [];
|
|
791
|
+
if (extensionFilters.length > 0) {
|
|
792
|
+
if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
|
|
793
|
+
code: "PRECONDITION_FAILED",
|
|
794
|
+
message: "CRUD extension filtering is not available"
|
|
795
|
+
});
|
|
796
|
+
const matchedIds$1 = await provider.matchEntityIds({
|
|
797
|
+
id: target.id,
|
|
798
|
+
filters: extensionFilters,
|
|
799
|
+
joinOperator: input.joinOperator,
|
|
800
|
+
limit: 5e3
|
|
801
|
+
});
|
|
802
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
803
|
+
}
|
|
804
|
+
if (search && provider?.searchEntityIds) {
|
|
805
|
+
const matchedIds$1 = await provider.searchEntityIds({
|
|
806
|
+
id: target.id,
|
|
807
|
+
search,
|
|
808
|
+
limit: 5e3
|
|
809
|
+
});
|
|
810
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
811
|
+
}
|
|
812
|
+
if (matchedSets.length === 0) return {
|
|
813
|
+
...input,
|
|
814
|
+
filters: baseFilters
|
|
815
|
+
};
|
|
816
|
+
let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
|
|
817
|
+
for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
|
|
818
|
+
return {
|
|
819
|
+
...input,
|
|
820
|
+
filters: [...baseFilters, {
|
|
821
|
+
id: idField,
|
|
822
|
+
value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
|
|
823
|
+
variant: "multiSelect",
|
|
824
|
+
operator: "inArray",
|
|
825
|
+
filterId: CRUD_EXTENSION_FILTER_ID
|
|
826
|
+
}]
|
|
827
|
+
};
|
|
828
|
+
}
|
|
612
829
|
/**
|
|
613
830
|
* 创建 CRUD Router
|
|
614
831
|
*
|
|
@@ -624,14 +841,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
624
841
|
function createCrudRouter(config) {
|
|
625
842
|
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
626
843
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
627
|
-
const entityOutputSchema = selectSchema;
|
|
844
|
+
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
628
845
|
const listOutputSchema = zod.z.object({
|
|
629
|
-
data: zod.z.array(
|
|
846
|
+
data: zod.z.array(entityOutputSchema),
|
|
630
847
|
total: zod.z.number(),
|
|
631
848
|
page: zod.z.number(),
|
|
632
849
|
perPage: zod.z.number(),
|
|
633
850
|
pageCount: zod.z.number()
|
|
634
851
|
});
|
|
852
|
+
const metadataOutputSchema = zod.z.object({
|
|
853
|
+
schema: zod.z.unknown().optional(),
|
|
854
|
+
fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
855
|
+
errors: zod.z.array(zod.z.string()).optional()
|
|
856
|
+
});
|
|
635
857
|
const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
|
|
636
858
|
const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
|
|
637
859
|
const upsertOutputSchema = zod.z.object({
|
|
@@ -639,12 +861,12 @@ function createCrudRouter(config) {
|
|
|
639
861
|
isNew: zod.z.boolean()
|
|
640
862
|
});
|
|
641
863
|
const exportOutputSchema = zod.z.object({
|
|
642
|
-
data: zod.z.array(
|
|
864
|
+
data: zod.z.array(entityOutputSchema),
|
|
643
865
|
total: zod.z.number(),
|
|
644
866
|
hasMore: zod.z.boolean()
|
|
645
867
|
});
|
|
646
868
|
const createManyOutputSchema = zod.z.object({
|
|
647
|
-
created: zod.z.array(
|
|
869
|
+
created: zod.z.array(entityOutputSchema),
|
|
648
870
|
count: zod.z.number()
|
|
649
871
|
});
|
|
650
872
|
const importOutputSchema = zod.z.object({
|
|
@@ -657,12 +879,14 @@ function createCrudRouter(config) {
|
|
|
657
879
|
}))
|
|
658
880
|
});
|
|
659
881
|
const resolved = resolveConfig(config);
|
|
882
|
+
const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
|
|
660
883
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
661
884
|
const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
|
|
662
885
|
const resolvedGetInputSchema = config.getInputSchema ? zod.z.union([zod.z.string(), config.getInputSchema]) : defaultGetInputSchema;
|
|
663
886
|
const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
|
|
664
887
|
const m = middleware;
|
|
665
888
|
const getIdColumn = () => table[idField];
|
|
889
|
+
const tableColumnNames = getTableColumnNames(table);
|
|
666
890
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
667
891
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
668
892
|
const conditions = [];
|
|
@@ -702,25 +926,38 @@ function createCrudRouter(config) {
|
|
|
702
926
|
});
|
|
703
927
|
};
|
|
704
928
|
const procedures = {
|
|
929
|
+
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
930
|
+
await withGuard(ctx, "list");
|
|
931
|
+
const target = readCrudTarget(config);
|
|
932
|
+
if (!target) return {};
|
|
933
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
934
|
+
if (!provider?.getMetadata) return {};
|
|
935
|
+
return provider.getMetadata({ id: target.id });
|
|
936
|
+
}),
|
|
705
937
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
706
938
|
await withGuard(ctx, "list");
|
|
707
939
|
const doList = async (listInput$1) => {
|
|
708
|
-
const
|
|
709
|
-
const
|
|
940
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
941
|
+
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
942
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
710
943
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
711
944
|
table,
|
|
712
945
|
filters: validatedFilters,
|
|
713
|
-
joinOperator:
|
|
714
|
-
resolveColumn: (columnId) =>
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
946
|
+
joinOperator: effectiveInput.joinOperator,
|
|
947
|
+
resolveColumn: (columnId) => {
|
|
948
|
+
const id = String(columnId);
|
|
949
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
950
|
+
return resolveColumnTarget({
|
|
951
|
+
table,
|
|
952
|
+
columnId: id,
|
|
953
|
+
allowedColumns: filterableColumns
|
|
954
|
+
});
|
|
955
|
+
}
|
|
719
956
|
}) : void 0);
|
|
720
957
|
let query = ctx.db.select().from(table).$dynamic();
|
|
721
958
|
if (where) query = query.where(where);
|
|
722
|
-
if (
|
|
723
|
-
const sortField =
|
|
959
|
+
if (effectiveInput.sort?.length) {
|
|
960
|
+
const sortField = effectiveInput.sort[0];
|
|
724
961
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
725
962
|
const column = resolveColumnTarget({
|
|
726
963
|
table,
|
|
@@ -730,16 +967,16 @@ function createCrudRouter(config) {
|
|
|
730
967
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
731
968
|
}
|
|
732
969
|
}
|
|
733
|
-
const
|
|
970
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
734
971
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
735
972
|
if (where) countQuery = countQuery.where(where);
|
|
736
973
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
737
974
|
return {
|
|
738
|
-
data,
|
|
975
|
+
data: enrichedData,
|
|
739
976
|
total: count,
|
|
740
|
-
page:
|
|
741
|
-
perPage:
|
|
742
|
-
pageCount: Math.ceil(count /
|
|
977
|
+
page: effectiveInput.page,
|
|
978
|
+
perPage: effectiveInput.perPage,
|
|
979
|
+
pageCount: Math.ceil(count / effectiveInput.perPage)
|
|
743
980
|
};
|
|
744
981
|
};
|
|
745
982
|
const listInput = input;
|
|
@@ -757,7 +994,9 @@ function createCrudRouter(config) {
|
|
|
757
994
|
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), id$1));
|
|
758
995
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
759
996
|
await withAuthorize(ctx, item, "get");
|
|
760
|
-
|
|
997
|
+
if (!item) return null;
|
|
998
|
+
const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
|
|
999
|
+
return enriched ?? null;
|
|
761
1000
|
};
|
|
762
1001
|
const id = typeof input === "string" ? input : input.id;
|
|
763
1002
|
if (m.get) return m.get({
|
|
@@ -771,12 +1010,15 @@ function createCrudRouter(config) {
|
|
|
771
1010
|
create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
|
|
772
1011
|
await withGuard(ctx, "create");
|
|
773
1012
|
const doCreate = async (inputData) => {
|
|
1013
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1014
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
774
1015
|
const injectData = resolved.getInject(ctx, "create");
|
|
775
1016
|
const data = {
|
|
776
|
-
...
|
|
1017
|
+
...splitInput.data,
|
|
777
1018
|
...injectData
|
|
778
1019
|
};
|
|
779
1020
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
1021
|
+
await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
|
|
780
1022
|
return created;
|
|
781
1023
|
};
|
|
782
1024
|
if (m.create) return m.create({
|
|
@@ -799,12 +1041,16 @@ function createCrudRouter(config) {
|
|
|
799
1041
|
message: "Resource not found or access denied"
|
|
800
1042
|
});
|
|
801
1043
|
const doUpdate = async (updateData) => {
|
|
1044
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1045
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
802
1046
|
const injectData = resolved.getInject(ctx, "update");
|
|
803
1047
|
const data = {
|
|
804
|
-
...
|
|
1048
|
+
...splitUpdate.data,
|
|
805
1049
|
...injectData
|
|
806
1050
|
};
|
|
807
|
-
|
|
1051
|
+
let updated = existing;
|
|
1052
|
+
if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
1053
|
+
await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
|
|
808
1054
|
return updated;
|
|
809
1055
|
};
|
|
810
1056
|
if (m.update) return m.update({
|
|
@@ -860,12 +1106,18 @@ function createCrudRouter(config) {
|
|
|
860
1106
|
await withGuard(ctx, "updateMany");
|
|
861
1107
|
const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
|
|
862
1108
|
const doUpdateMany = async (updateData) => {
|
|
1109
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1110
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
863
1111
|
const injectData = resolved.getInject(ctx, "update");
|
|
864
1112
|
const data = {
|
|
865
|
-
...
|
|
1113
|
+
...splitUpdate.data,
|
|
866
1114
|
...injectData
|
|
867
1115
|
};
|
|
868
|
-
|
|
1116
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1117
|
+
if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1118
|
+
else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1119
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
|
|
1120
|
+
return { updated: updated.length };
|
|
869
1121
|
};
|
|
870
1122
|
if (m.updateMany) return m.updateMany({
|
|
871
1123
|
ctx,
|
|
@@ -878,7 +1130,9 @@ function createCrudRouter(config) {
|
|
|
878
1130
|
upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
|
|
879
1131
|
await withGuard(ctx, "upsert");
|
|
880
1132
|
const doUpsert = async (inputData) => {
|
|
881
|
-
const
|
|
1133
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1134
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
1135
|
+
const inputId = splitInput.data[idField];
|
|
882
1136
|
let isNew = true;
|
|
883
1137
|
let existing = null;
|
|
884
1138
|
if (inputId) {
|
|
@@ -890,16 +1144,17 @@ function createCrudRouter(config) {
|
|
|
890
1144
|
}
|
|
891
1145
|
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
892
1146
|
const data = {
|
|
893
|
-
...
|
|
1147
|
+
...splitInput.data,
|
|
894
1148
|
...injectData
|
|
895
1149
|
};
|
|
896
1150
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
897
1151
|
target: getIdColumn(),
|
|
898
1152
|
set: {
|
|
899
|
-
...
|
|
1153
|
+
...splitInput.data,
|
|
900
1154
|
...resolved.getInject(ctx, "update")
|
|
901
1155
|
}
|
|
902
1156
|
}).returning();
|
|
1157
|
+
await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
|
|
903
1158
|
return {
|
|
904
1159
|
data: result,
|
|
905
1160
|
isNew
|
|
@@ -915,22 +1170,32 @@ function createCrudRouter(config) {
|
|
|
915
1170
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
916
1171
|
await withGuard(ctx, "export");
|
|
917
1172
|
const doExport = async (exportInput$1) => {
|
|
918
|
-
const
|
|
919
|
-
|
|
1173
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1174
|
+
page: 1,
|
|
1175
|
+
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1176
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
1177
|
+
...exportInput$1
|
|
1178
|
+
});
|
|
1179
|
+
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1180
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
920
1181
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
921
1182
|
table,
|
|
922
1183
|
filters: validatedFilters,
|
|
923
|
-
joinOperator:
|
|
924
|
-
resolveColumn: (columnId) =>
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1184
|
+
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
1185
|
+
resolveColumn: (columnId) => {
|
|
1186
|
+
const id = String(columnId);
|
|
1187
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
1188
|
+
return resolveColumnTarget({
|
|
1189
|
+
table,
|
|
1190
|
+
columnId: id,
|
|
1191
|
+
allowedColumns: filterableColumns
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
929
1194
|
}) : void 0);
|
|
930
1195
|
let query = ctx.db.select().from(table).$dynamic();
|
|
931
1196
|
if (where) query = query.where(where);
|
|
932
|
-
if (
|
|
933
|
-
const sortField =
|
|
1197
|
+
if (effectiveInput.sort?.length) {
|
|
1198
|
+
const sortField = effectiveInput.sort[0];
|
|
934
1199
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
935
1200
|
const column = resolveColumnTarget({
|
|
936
1201
|
table,
|
|
@@ -940,14 +1205,14 @@ function createCrudRouter(config) {
|
|
|
940
1205
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
941
1206
|
}
|
|
942
1207
|
}
|
|
943
|
-
const
|
|
1208
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
944
1209
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
945
1210
|
if (where) countQuery = countQuery.where(where);
|
|
946
1211
|
const total = (await countQuery)[0]?.count ?? 0;
|
|
947
1212
|
return {
|
|
948
|
-
data,
|
|
1213
|
+
data: enrichedData,
|
|
949
1214
|
total,
|
|
950
|
-
hasMore: total >
|
|
1215
|
+
hasMore: total > enrichedData.length
|
|
951
1216
|
};
|
|
952
1217
|
};
|
|
953
1218
|
const exportInput = input;
|
|
@@ -961,12 +1226,15 @@ function createCrudRouter(config) {
|
|
|
961
1226
|
createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
|
|
962
1227
|
await withGuard(ctx, "createMany");
|
|
963
1228
|
const doCreateMany = async (items) => {
|
|
1229
|
+
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1230
|
+
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
964
1231
|
const injectData = resolved.getInject(ctx, "create");
|
|
965
|
-
const values =
|
|
966
|
-
...
|
|
1232
|
+
const values = splitItems.map(({ data }) => ({
|
|
1233
|
+
...data,
|
|
967
1234
|
...injectData
|
|
968
1235
|
}));
|
|
969
1236
|
const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1237
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
|
|
970
1238
|
return {
|
|
971
1239
|
created,
|
|
972
1240
|
count: created.length
|
|
@@ -987,8 +1255,10 @@ function createCrudRouter(config) {
|
|
|
987
1255
|
for (let i = 0; i < importData$1.rows.length; i++) {
|
|
988
1256
|
const row = importData$1.rows[i];
|
|
989
1257
|
const result = schema.safeParse(row);
|
|
990
|
-
if (result.success)
|
|
991
|
-
|
|
1258
|
+
if (result.success) {
|
|
1259
|
+
const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
|
|
1260
|
+
validRows.push(splitRow);
|
|
1261
|
+
} else {
|
|
992
1262
|
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
993
1263
|
failed.push({
|
|
994
1264
|
row: i,
|
|
@@ -1008,26 +1278,30 @@ function createCrudRouter(config) {
|
|
|
1008
1278
|
skipped: 0,
|
|
1009
1279
|
failed
|
|
1010
1280
|
};
|
|
1281
|
+
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1011
1282
|
const injectData = resolved.getInject(ctx, "create");
|
|
1012
1283
|
const values = validRows.map((row) => ({
|
|
1013
|
-
...row,
|
|
1284
|
+
...row.data,
|
|
1014
1285
|
...injectData
|
|
1015
1286
|
}));
|
|
1016
1287
|
let insertedCount = 0;
|
|
1017
1288
|
let updatedCount = 0;
|
|
1018
1289
|
if (importData$1.onConflict === "upsert") {
|
|
1019
1290
|
const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where((0, drizzle_orm.inArray)(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
|
|
1020
|
-
const updateFields = Object.keys(validRows[0]);
|
|
1291
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
1021
1292
|
const setClause = { ...resolved.getInject(ctx, "update") };
|
|
1022
1293
|
for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
|
|
1023
1294
|
const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
|
|
1024
1295
|
target: getIdColumn(),
|
|
1025
1296
|
set: setClause
|
|
1026
1297
|
}).returning({ id: getIdColumn() });
|
|
1298
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
1027
1299
|
updatedCount = Math.min(existingCount, results.length);
|
|
1028
1300
|
insertedCount = results.length - updatedCount;
|
|
1029
1301
|
} else {
|
|
1030
|
-
|
|
1302
|
+
const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1303
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1304
|
+
insertedCount = inserted.length;
|
|
1031
1305
|
updatedCount = 0;
|
|
1032
1306
|
}
|
|
1033
1307
|
const skipped = validRows.length - insertedCount - updatedCount;
|