@wordrhyme/auto-crud-server 1.0.6 → 1.0.8
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 +311 -49
- package/dist/index.d.cts +76 -8
- package/dist/index.d.ts +69 -1
- package/dist/index.js +312 -50
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -457,6 +457,7 @@ function isProcedureMap(config) {
|
|
|
457
457
|
if (typeof config === "object" && config !== null) {
|
|
458
458
|
const keys = Object.keys(config);
|
|
459
459
|
const validKeys = [
|
|
460
|
+
"meta",
|
|
460
461
|
"list",
|
|
461
462
|
"get",
|
|
462
463
|
"create",
|
|
@@ -502,6 +503,9 @@ function resolveConfig(config) {
|
|
|
502
503
|
}
|
|
503
504
|
};
|
|
504
505
|
}
|
|
506
|
+
function resolveMetaProcedure(config, fallback) {
|
|
507
|
+
return isProcedureMap(config) ? config.meta ?? fallback : fallback;
|
|
508
|
+
}
|
|
505
509
|
const filterItemSchema = zod.z.object({
|
|
506
510
|
id: zod.z.string(),
|
|
507
511
|
value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
|
|
@@ -516,6 +520,7 @@ const baseListInputSchema = zod.z.object({
|
|
|
516
520
|
id: zod.z.string(),
|
|
517
521
|
desc: zod.z.boolean()
|
|
518
522
|
})).optional(),
|
|
523
|
+
search: zod.z.string().trim().optional(),
|
|
519
524
|
filters: zod.z.array(filterItemSchema).optional(),
|
|
520
525
|
joinOperator: zod.z.enum(["and", "or"]).default("and")
|
|
521
526
|
});
|
|
@@ -526,6 +531,7 @@ const baseExportInputSchema = zod.z.object({
|
|
|
526
531
|
id: zod.z.string(),
|
|
527
532
|
desc: zod.z.boolean()
|
|
528
533
|
})).optional(),
|
|
534
|
+
search: zod.z.string().trim().optional(),
|
|
529
535
|
filters: zod.z.array(filterItemSchema).optional(),
|
|
530
536
|
joinOperator: zod.z.enum(["and", "or"]).default("and"),
|
|
531
537
|
limit: zod.z.number().min(1).optional()
|
|
@@ -543,6 +549,13 @@ const DEFAULT_OMIT_FIELDS = [
|
|
|
543
549
|
"createdAt",
|
|
544
550
|
"updatedAt"
|
|
545
551
|
];
|
|
552
|
+
function shouldEnableCrudExtensionSchema(config) {
|
|
553
|
+
return Boolean(config.id) && config.extensions !== false;
|
|
554
|
+
}
|
|
555
|
+
function withCrudExtensionInput(schema, config) {
|
|
556
|
+
if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
|
|
557
|
+
return schema.passthrough();
|
|
558
|
+
}
|
|
546
559
|
/**
|
|
547
560
|
* 解析并派生 Schema
|
|
548
561
|
* - 优先使用显式传入的 Schema
|
|
@@ -560,11 +573,12 @@ function resolveSchemas(config) {
|
|
|
560
573
|
const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
|
|
561
574
|
schema = rawSchema.omit(omitConfig);
|
|
562
575
|
}
|
|
576
|
+
schema = withCrudExtensionInput(schema, config);
|
|
563
577
|
let selectSchema;
|
|
564
578
|
if (config.selectSchema) selectSchema = config.selectSchema;
|
|
565
579
|
else if (config.schema) selectSchema = schema;
|
|
566
580
|
else selectSchema = (0, drizzle_zod.createSelectSchema)(table);
|
|
567
|
-
const updateSchema = config.updateSchema
|
|
581
|
+
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
582
|
return {
|
|
569
583
|
selectSchema,
|
|
570
584
|
schema,
|
|
@@ -609,6 +623,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
609
623
|
}
|
|
610
624
|
return getTableColumn(table, columnId);
|
|
611
625
|
}
|
|
626
|
+
const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
|
|
627
|
+
const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
|
|
628
|
+
function isObjectRecord(value) {
|
|
629
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
630
|
+
}
|
|
631
|
+
function isCrudExtensionIdFilter(filter, idField) {
|
|
632
|
+
return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
|
|
633
|
+
}
|
|
634
|
+
function readCrudTarget(config) {
|
|
635
|
+
const id = config.id;
|
|
636
|
+
if (!id) return null;
|
|
637
|
+
return { id };
|
|
638
|
+
}
|
|
639
|
+
function resolveCrudExtensions(ctx, config) {
|
|
640
|
+
const extensions = config.extensions;
|
|
641
|
+
if (extensions === false) return null;
|
|
642
|
+
if (typeof extensions === "function") return extensions(ctx) ?? null;
|
|
643
|
+
if (extensions) return extensions;
|
|
644
|
+
if (!config.id) return null;
|
|
645
|
+
return ctx?.crudExtensions ?? null;
|
|
646
|
+
}
|
|
647
|
+
function getTableColumnNames(table) {
|
|
648
|
+
try {
|
|
649
|
+
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
650
|
+
const names = Object.keys(columns);
|
|
651
|
+
if (names.length > 0) return new Set(names);
|
|
652
|
+
} catch {}
|
|
653
|
+
return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
|
|
654
|
+
}
|
|
655
|
+
function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
656
|
+
if (!isObjectRecord(inputData)) return {
|
|
657
|
+
data: inputData,
|
|
658
|
+
rawValues: {},
|
|
659
|
+
baseValues: {},
|
|
660
|
+
extraValues: null
|
|
661
|
+
};
|
|
662
|
+
const baseValues = {};
|
|
663
|
+
const extraValues = {};
|
|
664
|
+
for (const [key, value] of Object.entries(inputData)) {
|
|
665
|
+
if (key === "ext") {
|
|
666
|
+
if (isObjectRecord(value)) Object.assign(extraValues, value);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (tableColumnNames.has(key)) baseValues[key] = value;
|
|
670
|
+
else extraValues[key] = value;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
data: baseValues,
|
|
674
|
+
rawValues: inputData,
|
|
675
|
+
baseValues,
|
|
676
|
+
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
680
|
+
const target = readCrudTarget(config);
|
|
681
|
+
const extraValues = input.extraValues;
|
|
682
|
+
if (!target || !extraValues) return;
|
|
683
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
684
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
685
|
+
if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
686
|
+
code: "PRECONDITION_FAILED",
|
|
687
|
+
message: "CRUD extension persistence is not available"
|
|
688
|
+
});
|
|
689
|
+
await provider.saveExtraValues({
|
|
690
|
+
id: target.id,
|
|
691
|
+
entityId: String(entityId),
|
|
692
|
+
rawValues: input.rawValues,
|
|
693
|
+
baseValues: input.baseValues,
|
|
694
|
+
extraValues,
|
|
695
|
+
tx: ctx?.tx
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
699
|
+
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
700
|
+
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
701
|
+
code: "PRECONDITION_FAILED",
|
|
702
|
+
message: "CRUD extension persistence is not available"
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
706
|
+
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
707
|
+
}
|
|
708
|
+
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
709
|
+
const extraByInputId = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const input of inputs) {
|
|
711
|
+
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
712
|
+
const inputId = input.data[idField];
|
|
713
|
+
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
714
|
+
}
|
|
715
|
+
const canUseIndexFallback = results.length === inputs.length;
|
|
716
|
+
await Promise.all(results.map((result, index) => {
|
|
717
|
+
const resultId = result[idField];
|
|
718
|
+
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
719
|
+
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
720
|
+
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
721
|
+
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
722
|
+
return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
function readProjectionDisplay(value) {
|
|
726
|
+
if (!isObjectRecord(value)) return value;
|
|
727
|
+
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
728
|
+
if ("value" in value) return value.value;
|
|
729
|
+
return value;
|
|
730
|
+
}
|
|
731
|
+
async function enrichCrudRows(ctx, config, idField, rows) {
|
|
732
|
+
const target = readCrudTarget(config);
|
|
733
|
+
if (!target || rows.length === 0) return rows;
|
|
734
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
735
|
+
if (!provider?.readProjection) return rows;
|
|
736
|
+
const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
|
|
737
|
+
if (entityIds.length === 0) return rows;
|
|
738
|
+
const projections = await provider.readProjection({
|
|
739
|
+
id: target.id,
|
|
740
|
+
entityIds
|
|
741
|
+
});
|
|
742
|
+
return rows.map((row) => {
|
|
743
|
+
const rowId = row[idField];
|
|
744
|
+
const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
|
|
745
|
+
if (!projected || Object.keys(projected).length === 0) return row;
|
|
746
|
+
return {
|
|
747
|
+
...row,
|
|
748
|
+
...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
|
|
749
|
+
};
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
753
|
+
if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
|
|
754
|
+
table,
|
|
755
|
+
columnId,
|
|
756
|
+
allowedColumns
|
|
757
|
+
}) !== void 0;
|
|
758
|
+
return getTableColumn(table, columnId) !== void 0;
|
|
759
|
+
}
|
|
760
|
+
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
761
|
+
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
762
|
+
}
|
|
763
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
764
|
+
const target = readCrudTarget(config);
|
|
765
|
+
const filters = input.filters ?? [];
|
|
766
|
+
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
767
|
+
if (!target || filters.length === 0 && !search) return input;
|
|
768
|
+
const baseFilters = [];
|
|
769
|
+
const extensionFilters = [];
|
|
770
|
+
for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
771
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
772
|
+
else extensionFilters.push(filter);
|
|
773
|
+
if (extensionFilters.length === 0 && !search) return {
|
|
774
|
+
...input,
|
|
775
|
+
filters: baseFilters
|
|
776
|
+
};
|
|
777
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
778
|
+
const matchedSets = [];
|
|
779
|
+
if (extensionFilters.length > 0) {
|
|
780
|
+
if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
|
|
781
|
+
code: "PRECONDITION_FAILED",
|
|
782
|
+
message: "CRUD extension filtering is not available"
|
|
783
|
+
});
|
|
784
|
+
const matchedIds$1 = await provider.matchEntityIds({
|
|
785
|
+
id: target.id,
|
|
786
|
+
filters: extensionFilters,
|
|
787
|
+
joinOperator: input.joinOperator,
|
|
788
|
+
limit: 5e3
|
|
789
|
+
});
|
|
790
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
791
|
+
}
|
|
792
|
+
if (search && provider?.searchEntityIds) {
|
|
793
|
+
const matchedIds$1 = await provider.searchEntityIds({
|
|
794
|
+
id: target.id,
|
|
795
|
+
search,
|
|
796
|
+
limit: 5e3
|
|
797
|
+
});
|
|
798
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
799
|
+
}
|
|
800
|
+
if (matchedSets.length === 0) return {
|
|
801
|
+
...input,
|
|
802
|
+
filters: baseFilters
|
|
803
|
+
};
|
|
804
|
+
let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
|
|
805
|
+
for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
|
|
806
|
+
return {
|
|
807
|
+
...input,
|
|
808
|
+
filters: [...baseFilters, {
|
|
809
|
+
id: idField,
|
|
810
|
+
value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
|
|
811
|
+
variant: "multiSelect",
|
|
812
|
+
operator: "inArray",
|
|
813
|
+
filterId: CRUD_EXTENSION_FILTER_ID
|
|
814
|
+
}]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
612
817
|
/**
|
|
613
818
|
* 创建 CRUD Router
|
|
614
819
|
*
|
|
@@ -624,14 +829,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
624
829
|
function createCrudRouter(config) {
|
|
625
830
|
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
626
831
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
627
|
-
const entityOutputSchema = selectSchema;
|
|
832
|
+
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
628
833
|
const listOutputSchema = zod.z.object({
|
|
629
|
-
data: zod.z.array(
|
|
834
|
+
data: zod.z.array(entityOutputSchema),
|
|
630
835
|
total: zod.z.number(),
|
|
631
836
|
page: zod.z.number(),
|
|
632
837
|
perPage: zod.z.number(),
|
|
633
838
|
pageCount: zod.z.number()
|
|
634
839
|
});
|
|
840
|
+
const metadataOutputSchema = zod.z.object({
|
|
841
|
+
schema: zod.z.unknown().optional(),
|
|
842
|
+
fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
843
|
+
errors: zod.z.array(zod.z.string()).optional()
|
|
844
|
+
});
|
|
635
845
|
const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
|
|
636
846
|
const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
|
|
637
847
|
const upsertOutputSchema = zod.z.object({
|
|
@@ -639,12 +849,12 @@ function createCrudRouter(config) {
|
|
|
639
849
|
isNew: zod.z.boolean()
|
|
640
850
|
});
|
|
641
851
|
const exportOutputSchema = zod.z.object({
|
|
642
|
-
data: zod.z.array(
|
|
852
|
+
data: zod.z.array(entityOutputSchema),
|
|
643
853
|
total: zod.z.number(),
|
|
644
854
|
hasMore: zod.z.boolean()
|
|
645
855
|
});
|
|
646
856
|
const createManyOutputSchema = zod.z.object({
|
|
647
|
-
created: zod.z.array(
|
|
857
|
+
created: zod.z.array(entityOutputSchema),
|
|
648
858
|
count: zod.z.number()
|
|
649
859
|
});
|
|
650
860
|
const importOutputSchema = zod.z.object({
|
|
@@ -657,12 +867,14 @@ function createCrudRouter(config) {
|
|
|
657
867
|
}))
|
|
658
868
|
});
|
|
659
869
|
const resolved = resolveConfig(config);
|
|
870
|
+
const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
|
|
660
871
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
661
872
|
const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
|
|
662
873
|
const resolvedGetInputSchema = config.getInputSchema ? zod.z.union([zod.z.string(), config.getInputSchema]) : defaultGetInputSchema;
|
|
663
874
|
const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
|
|
664
875
|
const m = middleware;
|
|
665
876
|
const getIdColumn = () => table[idField];
|
|
877
|
+
const tableColumnNames = getTableColumnNames(table);
|
|
666
878
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
667
879
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
668
880
|
const conditions = [];
|
|
@@ -702,25 +914,38 @@ function createCrudRouter(config) {
|
|
|
702
914
|
});
|
|
703
915
|
};
|
|
704
916
|
const procedures = {
|
|
917
|
+
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
918
|
+
await withGuard(ctx, "list");
|
|
919
|
+
const target = readCrudTarget(config);
|
|
920
|
+
if (!target) return {};
|
|
921
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
922
|
+
if (!provider?.getMetadata) return {};
|
|
923
|
+
return provider.getMetadata({ id: target.id });
|
|
924
|
+
}),
|
|
705
925
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
706
926
|
await withGuard(ctx, "list");
|
|
707
927
|
const doList = async (listInput$1) => {
|
|
708
|
-
const
|
|
709
|
-
const
|
|
928
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
929
|
+
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
930
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
710
931
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
711
932
|
table,
|
|
712
933
|
filters: validatedFilters,
|
|
713
|
-
joinOperator:
|
|
714
|
-
resolveColumn: (columnId) =>
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
934
|
+
joinOperator: effectiveInput.joinOperator,
|
|
935
|
+
resolveColumn: (columnId) => {
|
|
936
|
+
const id = String(columnId);
|
|
937
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
938
|
+
return resolveColumnTarget({
|
|
939
|
+
table,
|
|
940
|
+
columnId: id,
|
|
941
|
+
allowedColumns: filterableColumns
|
|
942
|
+
});
|
|
943
|
+
}
|
|
719
944
|
}) : void 0);
|
|
720
945
|
let query = ctx.db.select().from(table).$dynamic();
|
|
721
946
|
if (where) query = query.where(where);
|
|
722
|
-
if (
|
|
723
|
-
const sortField =
|
|
947
|
+
if (effectiveInput.sort?.length) {
|
|
948
|
+
const sortField = effectiveInput.sort[0];
|
|
724
949
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
725
950
|
const column = resolveColumnTarget({
|
|
726
951
|
table,
|
|
@@ -730,16 +955,16 @@ function createCrudRouter(config) {
|
|
|
730
955
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
731
956
|
}
|
|
732
957
|
}
|
|
733
|
-
const
|
|
958
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
734
959
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
735
960
|
if (where) countQuery = countQuery.where(where);
|
|
736
961
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
737
962
|
return {
|
|
738
|
-
data,
|
|
963
|
+
data: enrichedData,
|
|
739
964
|
total: count,
|
|
740
|
-
page:
|
|
741
|
-
perPage:
|
|
742
|
-
pageCount: Math.ceil(count /
|
|
965
|
+
page: effectiveInput.page,
|
|
966
|
+
perPage: effectiveInput.perPage,
|
|
967
|
+
pageCount: Math.ceil(count / effectiveInput.perPage)
|
|
743
968
|
};
|
|
744
969
|
};
|
|
745
970
|
const listInput = input;
|
|
@@ -757,7 +982,9 @@ function createCrudRouter(config) {
|
|
|
757
982
|
const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), id$1));
|
|
758
983
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
759
984
|
await withAuthorize(ctx, item, "get");
|
|
760
|
-
|
|
985
|
+
if (!item) return null;
|
|
986
|
+
const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
|
|
987
|
+
return enriched ?? null;
|
|
761
988
|
};
|
|
762
989
|
const id = typeof input === "string" ? input : input.id;
|
|
763
990
|
if (m.get) return m.get({
|
|
@@ -771,12 +998,15 @@ function createCrudRouter(config) {
|
|
|
771
998
|
create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
|
|
772
999
|
await withGuard(ctx, "create");
|
|
773
1000
|
const doCreate = async (inputData) => {
|
|
1001
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1002
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
774
1003
|
const injectData = resolved.getInject(ctx, "create");
|
|
775
1004
|
const data = {
|
|
776
|
-
...
|
|
1005
|
+
...splitInput.data,
|
|
777
1006
|
...injectData
|
|
778
1007
|
};
|
|
779
1008
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
1009
|
+
await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
|
|
780
1010
|
return created;
|
|
781
1011
|
};
|
|
782
1012
|
if (m.create) return m.create({
|
|
@@ -799,12 +1029,16 @@ function createCrudRouter(config) {
|
|
|
799
1029
|
message: "Resource not found or access denied"
|
|
800
1030
|
});
|
|
801
1031
|
const doUpdate = async (updateData) => {
|
|
1032
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1033
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
802
1034
|
const injectData = resolved.getInject(ctx, "update");
|
|
803
1035
|
const data = {
|
|
804
|
-
...
|
|
1036
|
+
...splitUpdate.data,
|
|
805
1037
|
...injectData
|
|
806
1038
|
};
|
|
807
|
-
|
|
1039
|
+
let updated = existing;
|
|
1040
|
+
if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
1041
|
+
await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
|
|
808
1042
|
return updated;
|
|
809
1043
|
};
|
|
810
1044
|
if (m.update) return m.update({
|
|
@@ -860,12 +1094,18 @@ function createCrudRouter(config) {
|
|
|
860
1094
|
await withGuard(ctx, "updateMany");
|
|
861
1095
|
const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
|
|
862
1096
|
const doUpdateMany = async (updateData) => {
|
|
1097
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1098
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
863
1099
|
const injectData = resolved.getInject(ctx, "update");
|
|
864
1100
|
const data = {
|
|
865
|
-
...
|
|
1101
|
+
...splitUpdate.data,
|
|
866
1102
|
...injectData
|
|
867
1103
|
};
|
|
868
|
-
|
|
1104
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1105
|
+
if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1106
|
+
else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1107
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
|
|
1108
|
+
return { updated: updated.length };
|
|
869
1109
|
};
|
|
870
1110
|
if (m.updateMany) return m.updateMany({
|
|
871
1111
|
ctx,
|
|
@@ -878,7 +1118,9 @@ function createCrudRouter(config) {
|
|
|
878
1118
|
upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
|
|
879
1119
|
await withGuard(ctx, "upsert");
|
|
880
1120
|
const doUpsert = async (inputData) => {
|
|
881
|
-
const
|
|
1121
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1122
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
1123
|
+
const inputId = splitInput.data[idField];
|
|
882
1124
|
let isNew = true;
|
|
883
1125
|
let existing = null;
|
|
884
1126
|
if (inputId) {
|
|
@@ -890,16 +1132,17 @@ function createCrudRouter(config) {
|
|
|
890
1132
|
}
|
|
891
1133
|
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
892
1134
|
const data = {
|
|
893
|
-
...
|
|
1135
|
+
...splitInput.data,
|
|
894
1136
|
...injectData
|
|
895
1137
|
};
|
|
896
1138
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
897
1139
|
target: getIdColumn(),
|
|
898
1140
|
set: {
|
|
899
|
-
...
|
|
1141
|
+
...splitInput.data,
|
|
900
1142
|
...resolved.getInject(ctx, "update")
|
|
901
1143
|
}
|
|
902
1144
|
}).returning();
|
|
1145
|
+
await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
|
|
903
1146
|
return {
|
|
904
1147
|
data: result,
|
|
905
1148
|
isNew
|
|
@@ -915,22 +1158,32 @@ function createCrudRouter(config) {
|
|
|
915
1158
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
916
1159
|
await withGuard(ctx, "export");
|
|
917
1160
|
const doExport = async (exportInput$1) => {
|
|
918
|
-
const
|
|
919
|
-
|
|
1161
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1162
|
+
page: 1,
|
|
1163
|
+
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1164
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
1165
|
+
...exportInput$1
|
|
1166
|
+
});
|
|
1167
|
+
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1168
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
920
1169
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
921
1170
|
table,
|
|
922
1171
|
filters: validatedFilters,
|
|
923
|
-
joinOperator:
|
|
924
|
-
resolveColumn: (columnId) =>
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1172
|
+
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
1173
|
+
resolveColumn: (columnId) => {
|
|
1174
|
+
const id = String(columnId);
|
|
1175
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
1176
|
+
return resolveColumnTarget({
|
|
1177
|
+
table,
|
|
1178
|
+
columnId: id,
|
|
1179
|
+
allowedColumns: filterableColumns
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
929
1182
|
}) : void 0);
|
|
930
1183
|
let query = ctx.db.select().from(table).$dynamic();
|
|
931
1184
|
if (where) query = query.where(where);
|
|
932
|
-
if (
|
|
933
|
-
const sortField =
|
|
1185
|
+
if (effectiveInput.sort?.length) {
|
|
1186
|
+
const sortField = effectiveInput.sort[0];
|
|
934
1187
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
935
1188
|
const column = resolveColumnTarget({
|
|
936
1189
|
table,
|
|
@@ -940,14 +1193,14 @@ function createCrudRouter(config) {
|
|
|
940
1193
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
941
1194
|
}
|
|
942
1195
|
}
|
|
943
|
-
const
|
|
1196
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
944
1197
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
945
1198
|
if (where) countQuery = countQuery.where(where);
|
|
946
1199
|
const total = (await countQuery)[0]?.count ?? 0;
|
|
947
1200
|
return {
|
|
948
|
-
data,
|
|
1201
|
+
data: enrichedData,
|
|
949
1202
|
total,
|
|
950
|
-
hasMore: total >
|
|
1203
|
+
hasMore: total > enrichedData.length
|
|
951
1204
|
};
|
|
952
1205
|
};
|
|
953
1206
|
const exportInput = input;
|
|
@@ -961,12 +1214,15 @@ function createCrudRouter(config) {
|
|
|
961
1214
|
createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
|
|
962
1215
|
await withGuard(ctx, "createMany");
|
|
963
1216
|
const doCreateMany = async (items) => {
|
|
1217
|
+
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1218
|
+
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
964
1219
|
const injectData = resolved.getInject(ctx, "create");
|
|
965
|
-
const values =
|
|
966
|
-
...
|
|
1220
|
+
const values = splitItems.map(({ data }) => ({
|
|
1221
|
+
...data,
|
|
967
1222
|
...injectData
|
|
968
1223
|
}));
|
|
969
1224
|
const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1225
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
|
|
970
1226
|
return {
|
|
971
1227
|
created,
|
|
972
1228
|
count: created.length
|
|
@@ -987,8 +1243,10 @@ function createCrudRouter(config) {
|
|
|
987
1243
|
for (let i = 0; i < importData$1.rows.length; i++) {
|
|
988
1244
|
const row = importData$1.rows[i];
|
|
989
1245
|
const result = schema.safeParse(row);
|
|
990
|
-
if (result.success)
|
|
991
|
-
|
|
1246
|
+
if (result.success) {
|
|
1247
|
+
const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
|
|
1248
|
+
validRows.push(splitRow);
|
|
1249
|
+
} else {
|
|
992
1250
|
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
993
1251
|
failed.push({
|
|
994
1252
|
row: i,
|
|
@@ -1008,26 +1266,30 @@ function createCrudRouter(config) {
|
|
|
1008
1266
|
skipped: 0,
|
|
1009
1267
|
failed
|
|
1010
1268
|
};
|
|
1269
|
+
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1011
1270
|
const injectData = resolved.getInject(ctx, "create");
|
|
1012
1271
|
const values = validRows.map((row) => ({
|
|
1013
|
-
...row,
|
|
1272
|
+
...row.data,
|
|
1014
1273
|
...injectData
|
|
1015
1274
|
}));
|
|
1016
1275
|
let insertedCount = 0;
|
|
1017
1276
|
let updatedCount = 0;
|
|
1018
1277
|
if (importData$1.onConflict === "upsert") {
|
|
1019
1278
|
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]);
|
|
1279
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
1021
1280
|
const setClause = { ...resolved.getInject(ctx, "update") };
|
|
1022
1281
|
for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
|
|
1023
1282
|
const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
|
|
1024
1283
|
target: getIdColumn(),
|
|
1025
1284
|
set: setClause
|
|
1026
1285
|
}).returning({ id: getIdColumn() });
|
|
1286
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
1027
1287
|
updatedCount = Math.min(existingCount, results.length);
|
|
1028
1288
|
insertedCount = results.length - updatedCount;
|
|
1029
1289
|
} else {
|
|
1030
|
-
|
|
1290
|
+
const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1291
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1292
|
+
insertedCount = inserted.length;
|
|
1031
1293
|
updatedCount = 0;
|
|
1032
1294
|
}
|
|
1033
1295
|
const skipped = validRows.length - insertedCount - updatedCount;
|