@wordrhyme/auto-crud-server 1.0.7 → 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 +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +312 -50
- package/package.json +5 -5
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";
|
|
@@ -428,6 +428,7 @@ function isProcedureMap(config) {
|
|
|
428
428
|
if (typeof config === "object" && config !== null) {
|
|
429
429
|
const keys = Object.keys(config);
|
|
430
430
|
const validKeys = [
|
|
431
|
+
"meta",
|
|
431
432
|
"list",
|
|
432
433
|
"get",
|
|
433
434
|
"create",
|
|
@@ -473,6 +474,9 @@ function resolveConfig(config) {
|
|
|
473
474
|
}
|
|
474
475
|
};
|
|
475
476
|
}
|
|
477
|
+
function resolveMetaProcedure(config, fallback) {
|
|
478
|
+
return isProcedureMap(config) ? config.meta ?? fallback : fallback;
|
|
479
|
+
}
|
|
476
480
|
const filterItemSchema = z.object({
|
|
477
481
|
id: z.string(),
|
|
478
482
|
value: z.union([z.string(), z.array(z.string())]),
|
|
@@ -487,6 +491,7 @@ const baseListInputSchema = z.object({
|
|
|
487
491
|
id: z.string(),
|
|
488
492
|
desc: z.boolean()
|
|
489
493
|
})).optional(),
|
|
494
|
+
search: z.string().trim().optional(),
|
|
490
495
|
filters: z.array(filterItemSchema).optional(),
|
|
491
496
|
joinOperator: z.enum(["and", "or"]).default("and")
|
|
492
497
|
});
|
|
@@ -497,6 +502,7 @@ const baseExportInputSchema = z.object({
|
|
|
497
502
|
id: z.string(),
|
|
498
503
|
desc: z.boolean()
|
|
499
504
|
})).optional(),
|
|
505
|
+
search: z.string().trim().optional(),
|
|
500
506
|
filters: z.array(filterItemSchema).optional(),
|
|
501
507
|
joinOperator: z.enum(["and", "or"]).default("and"),
|
|
502
508
|
limit: z.number().min(1).optional()
|
|
@@ -514,6 +520,13 @@ const DEFAULT_OMIT_FIELDS = [
|
|
|
514
520
|
"createdAt",
|
|
515
521
|
"updatedAt"
|
|
516
522
|
];
|
|
523
|
+
function shouldEnableCrudExtensionSchema(config) {
|
|
524
|
+
return Boolean(config.id) && config.extensions !== false;
|
|
525
|
+
}
|
|
526
|
+
function withCrudExtensionInput(schema, config) {
|
|
527
|
+
if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
|
|
528
|
+
return schema.passthrough();
|
|
529
|
+
}
|
|
517
530
|
/**
|
|
518
531
|
* 解析并派生 Schema
|
|
519
532
|
* - 优先使用显式传入的 Schema
|
|
@@ -531,11 +544,12 @@ function resolveSchemas(config) {
|
|
|
531
544
|
const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
|
|
532
545
|
schema = rawSchema.omit(omitConfig);
|
|
533
546
|
}
|
|
547
|
+
schema = withCrudExtensionInput(schema, config);
|
|
534
548
|
let selectSchema;
|
|
535
549
|
if (config.selectSchema) selectSchema = config.selectSchema;
|
|
536
550
|
else if (config.schema) selectSchema = schema;
|
|
537
551
|
else selectSchema = createSelectSchema(table);
|
|
538
|
-
const updateSchema = config.updateSchema
|
|
552
|
+
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
553
|
return {
|
|
540
554
|
selectSchema,
|
|
541
555
|
schema,
|
|
@@ -580,6 +594,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
580
594
|
}
|
|
581
595
|
return getTableColumn(table, columnId);
|
|
582
596
|
}
|
|
597
|
+
const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
|
|
598
|
+
const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
|
|
599
|
+
function isObjectRecord(value) {
|
|
600
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
601
|
+
}
|
|
602
|
+
function isCrudExtensionIdFilter(filter, idField) {
|
|
603
|
+
return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
|
|
604
|
+
}
|
|
605
|
+
function readCrudTarget(config) {
|
|
606
|
+
const id = config.id;
|
|
607
|
+
if (!id) return null;
|
|
608
|
+
return { id };
|
|
609
|
+
}
|
|
610
|
+
function resolveCrudExtensions(ctx, config) {
|
|
611
|
+
const extensions = config.extensions;
|
|
612
|
+
if (extensions === false) return null;
|
|
613
|
+
if (typeof extensions === "function") return extensions(ctx) ?? null;
|
|
614
|
+
if (extensions) return extensions;
|
|
615
|
+
if (!config.id) return null;
|
|
616
|
+
return ctx?.crudExtensions ?? null;
|
|
617
|
+
}
|
|
618
|
+
function getTableColumnNames(table) {
|
|
619
|
+
try {
|
|
620
|
+
const columns = getTableColumns(table);
|
|
621
|
+
const names = Object.keys(columns);
|
|
622
|
+
if (names.length > 0) return new Set(names);
|
|
623
|
+
} catch {}
|
|
624
|
+
return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
|
|
625
|
+
}
|
|
626
|
+
function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
627
|
+
if (!isObjectRecord(inputData)) return {
|
|
628
|
+
data: inputData,
|
|
629
|
+
rawValues: {},
|
|
630
|
+
baseValues: {},
|
|
631
|
+
extraValues: null
|
|
632
|
+
};
|
|
633
|
+
const baseValues = {};
|
|
634
|
+
const extraValues = {};
|
|
635
|
+
for (const [key, value] of Object.entries(inputData)) {
|
|
636
|
+
if (key === "ext") {
|
|
637
|
+
if (isObjectRecord(value)) Object.assign(extraValues, value);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (tableColumnNames.has(key)) baseValues[key] = value;
|
|
641
|
+
else extraValues[key] = value;
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
data: baseValues,
|
|
645
|
+
rawValues: inputData,
|
|
646
|
+
baseValues,
|
|
647
|
+
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
651
|
+
const target = readCrudTarget(config);
|
|
652
|
+
const extraValues = input.extraValues;
|
|
653
|
+
if (!target || !extraValues) return;
|
|
654
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
655
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
656
|
+
if (!provider?.saveExtraValues) throw new TRPCError({
|
|
657
|
+
code: "PRECONDITION_FAILED",
|
|
658
|
+
message: "CRUD extension persistence is not available"
|
|
659
|
+
});
|
|
660
|
+
await provider.saveExtraValues({
|
|
661
|
+
id: target.id,
|
|
662
|
+
entityId: String(entityId),
|
|
663
|
+
rawValues: input.rawValues,
|
|
664
|
+
baseValues: input.baseValues,
|
|
665
|
+
extraValues,
|
|
666
|
+
tx: ctx?.tx
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
670
|
+
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
671
|
+
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new TRPCError({
|
|
672
|
+
code: "PRECONDITION_FAILED",
|
|
673
|
+
message: "CRUD extension persistence is not available"
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
677
|
+
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
678
|
+
}
|
|
679
|
+
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
680
|
+
const extraByInputId = /* @__PURE__ */ new Map();
|
|
681
|
+
for (const input of inputs) {
|
|
682
|
+
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
683
|
+
const inputId = input.data[idField];
|
|
684
|
+
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
685
|
+
}
|
|
686
|
+
const canUseIndexFallback = results.length === inputs.length;
|
|
687
|
+
await Promise.all(results.map((result, index) => {
|
|
688
|
+
const resultId = result[idField];
|
|
689
|
+
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
690
|
+
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
691
|
+
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
692
|
+
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
693
|
+
return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
function readProjectionDisplay(value) {
|
|
697
|
+
if (!isObjectRecord(value)) return value;
|
|
698
|
+
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
699
|
+
if ("value" in value) return value.value;
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
async function enrichCrudRows(ctx, config, idField, rows) {
|
|
703
|
+
const target = readCrudTarget(config);
|
|
704
|
+
if (!target || rows.length === 0) return rows;
|
|
705
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
706
|
+
if (!provider?.readProjection) return rows;
|
|
707
|
+
const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
|
|
708
|
+
if (entityIds.length === 0) return rows;
|
|
709
|
+
const projections = await provider.readProjection({
|
|
710
|
+
id: target.id,
|
|
711
|
+
entityIds
|
|
712
|
+
});
|
|
713
|
+
return rows.map((row) => {
|
|
714
|
+
const rowId = row[idField];
|
|
715
|
+
const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
|
|
716
|
+
if (!projected || Object.keys(projected).length === 0) return row;
|
|
717
|
+
return {
|
|
718
|
+
...row,
|
|
719
|
+
...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
724
|
+
if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
|
|
725
|
+
table,
|
|
726
|
+
columnId,
|
|
727
|
+
allowedColumns
|
|
728
|
+
}) !== void 0;
|
|
729
|
+
return getTableColumn(table, columnId) !== void 0;
|
|
730
|
+
}
|
|
731
|
+
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
732
|
+
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
733
|
+
}
|
|
734
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
735
|
+
const target = readCrudTarget(config);
|
|
736
|
+
const filters = input.filters ?? [];
|
|
737
|
+
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
738
|
+
if (!target || filters.length === 0 && !search) return input;
|
|
739
|
+
const baseFilters = [];
|
|
740
|
+
const extensionFilters = [];
|
|
741
|
+
for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
742
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
743
|
+
else extensionFilters.push(filter);
|
|
744
|
+
if (extensionFilters.length === 0 && !search) return {
|
|
745
|
+
...input,
|
|
746
|
+
filters: baseFilters
|
|
747
|
+
};
|
|
748
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
749
|
+
const matchedSets = [];
|
|
750
|
+
if (extensionFilters.length > 0) {
|
|
751
|
+
if (!provider?.matchEntityIds) throw new TRPCError({
|
|
752
|
+
code: "PRECONDITION_FAILED",
|
|
753
|
+
message: "CRUD extension filtering is not available"
|
|
754
|
+
});
|
|
755
|
+
const matchedIds$1 = await provider.matchEntityIds({
|
|
756
|
+
id: target.id,
|
|
757
|
+
filters: extensionFilters,
|
|
758
|
+
joinOperator: input.joinOperator,
|
|
759
|
+
limit: 5e3
|
|
760
|
+
});
|
|
761
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
762
|
+
}
|
|
763
|
+
if (search && provider?.searchEntityIds) {
|
|
764
|
+
const matchedIds$1 = await provider.searchEntityIds({
|
|
765
|
+
id: target.id,
|
|
766
|
+
search,
|
|
767
|
+
limit: 5e3
|
|
768
|
+
});
|
|
769
|
+
matchedSets.push(new Set(matchedIds$1));
|
|
770
|
+
}
|
|
771
|
+
if (matchedSets.length === 0) return {
|
|
772
|
+
...input,
|
|
773
|
+
filters: baseFilters
|
|
774
|
+
};
|
|
775
|
+
let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
|
|
776
|
+
for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
|
|
777
|
+
return {
|
|
778
|
+
...input,
|
|
779
|
+
filters: [...baseFilters, {
|
|
780
|
+
id: idField,
|
|
781
|
+
value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
|
|
782
|
+
variant: "multiSelect",
|
|
783
|
+
operator: "inArray",
|
|
784
|
+
filterId: CRUD_EXTENSION_FILTER_ID
|
|
785
|
+
}]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
583
788
|
/**
|
|
584
789
|
* 创建 CRUD Router
|
|
585
790
|
*
|
|
@@ -595,14 +800,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
|
595
800
|
function createCrudRouter(config) {
|
|
596
801
|
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
597
802
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
598
|
-
const entityOutputSchema = selectSchema;
|
|
803
|
+
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
599
804
|
const listOutputSchema = z.object({
|
|
600
|
-
data: z.array(
|
|
805
|
+
data: z.array(entityOutputSchema),
|
|
601
806
|
total: z.number(),
|
|
602
807
|
page: z.number(),
|
|
603
808
|
perPage: z.number(),
|
|
604
809
|
pageCount: z.number()
|
|
605
810
|
});
|
|
811
|
+
const metadataOutputSchema = z.object({
|
|
812
|
+
schema: z.unknown().optional(),
|
|
813
|
+
fields: z.record(z.string(), z.unknown()).optional(),
|
|
814
|
+
errors: z.array(z.string()).optional()
|
|
815
|
+
});
|
|
606
816
|
const deleteManyOutputSchema = z.object({ deleted: z.number() });
|
|
607
817
|
const updateManyOutputSchema = z.object({ updated: z.number() });
|
|
608
818
|
const upsertOutputSchema = z.object({
|
|
@@ -610,12 +820,12 @@ function createCrudRouter(config) {
|
|
|
610
820
|
isNew: z.boolean()
|
|
611
821
|
});
|
|
612
822
|
const exportOutputSchema = z.object({
|
|
613
|
-
data: z.array(
|
|
823
|
+
data: z.array(entityOutputSchema),
|
|
614
824
|
total: z.number(),
|
|
615
825
|
hasMore: z.boolean()
|
|
616
826
|
});
|
|
617
827
|
const createManyOutputSchema = z.object({
|
|
618
|
-
created: z.array(
|
|
828
|
+
created: z.array(entityOutputSchema),
|
|
619
829
|
count: z.number()
|
|
620
830
|
});
|
|
621
831
|
const importOutputSchema = z.object({
|
|
@@ -628,12 +838,14 @@ function createCrudRouter(config) {
|
|
|
628
838
|
}))
|
|
629
839
|
});
|
|
630
840
|
const resolved = resolveConfig(config);
|
|
841
|
+
const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
|
|
631
842
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
632
843
|
const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
|
|
633
844
|
const resolvedGetInputSchema = config.getInputSchema ? z.union([z.string(), config.getInputSchema]) : defaultGetInputSchema;
|
|
634
845
|
const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
|
|
635
846
|
const m = middleware;
|
|
636
847
|
const getIdColumn = () => table[idField];
|
|
848
|
+
const tableColumnNames = getTableColumnNames(table);
|
|
637
849
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
638
850
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
639
851
|
const conditions = [];
|
|
@@ -673,25 +885,38 @@ function createCrudRouter(config) {
|
|
|
673
885
|
});
|
|
674
886
|
};
|
|
675
887
|
const procedures = {
|
|
888
|
+
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
889
|
+
await withGuard(ctx, "list");
|
|
890
|
+
const target = readCrudTarget(config);
|
|
891
|
+
if (!target) return {};
|
|
892
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
893
|
+
if (!provider?.getMetadata) return {};
|
|
894
|
+
return provider.getMetadata({ id: target.id });
|
|
895
|
+
}),
|
|
676
896
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
677
897
|
await withGuard(ctx, "list");
|
|
678
898
|
const doList = async (listInput$1) => {
|
|
679
|
-
const
|
|
680
|
-
const
|
|
899
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
900
|
+
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
901
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
681
902
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
682
903
|
table,
|
|
683
904
|
filters: validatedFilters,
|
|
684
|
-
joinOperator:
|
|
685
|
-
resolveColumn: (columnId) =>
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
905
|
+
joinOperator: effectiveInput.joinOperator,
|
|
906
|
+
resolveColumn: (columnId) => {
|
|
907
|
+
const id = String(columnId);
|
|
908
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
909
|
+
return resolveColumnTarget({
|
|
910
|
+
table,
|
|
911
|
+
columnId: id,
|
|
912
|
+
allowedColumns: filterableColumns
|
|
913
|
+
});
|
|
914
|
+
}
|
|
690
915
|
}) : void 0);
|
|
691
916
|
let query = ctx.db.select().from(table).$dynamic();
|
|
692
917
|
if (where) query = query.where(where);
|
|
693
|
-
if (
|
|
694
|
-
const sortField =
|
|
918
|
+
if (effectiveInput.sort?.length) {
|
|
919
|
+
const sortField = effectiveInput.sort[0];
|
|
695
920
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
696
921
|
const column = resolveColumnTarget({
|
|
697
922
|
table,
|
|
@@ -701,16 +926,16 @@ function createCrudRouter(config) {
|
|
|
701
926
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
702
927
|
}
|
|
703
928
|
}
|
|
704
|
-
const
|
|
929
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
705
930
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
706
931
|
if (where) countQuery = countQuery.where(where);
|
|
707
932
|
const count = (await countQuery)[0]?.count ?? 0;
|
|
708
933
|
return {
|
|
709
|
-
data,
|
|
934
|
+
data: enrichedData,
|
|
710
935
|
total: count,
|
|
711
|
-
page:
|
|
712
|
-
perPage:
|
|
713
|
-
pageCount: Math.ceil(count /
|
|
936
|
+
page: effectiveInput.page,
|
|
937
|
+
perPage: effectiveInput.perPage,
|
|
938
|
+
pageCount: Math.ceil(count / effectiveInput.perPage)
|
|
714
939
|
};
|
|
715
940
|
};
|
|
716
941
|
const listInput = input;
|
|
@@ -728,7 +953,9 @@ function createCrudRouter(config) {
|
|
|
728
953
|
const where = buildWhere(ctx, "get", eq(getIdColumn(), id$1));
|
|
729
954
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
730
955
|
await withAuthorize(ctx, item, "get");
|
|
731
|
-
|
|
956
|
+
if (!item) return null;
|
|
957
|
+
const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
|
|
958
|
+
return enriched ?? null;
|
|
732
959
|
};
|
|
733
960
|
const id = typeof input === "string" ? input : input.id;
|
|
734
961
|
if (m.get) return m.get({
|
|
@@ -742,12 +969,15 @@ function createCrudRouter(config) {
|
|
|
742
969
|
create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
|
|
743
970
|
await withGuard(ctx, "create");
|
|
744
971
|
const doCreate = async (inputData) => {
|
|
972
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
973
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
745
974
|
const injectData = resolved.getInject(ctx, "create");
|
|
746
975
|
const data = {
|
|
747
|
-
...
|
|
976
|
+
...splitInput.data,
|
|
748
977
|
...injectData
|
|
749
978
|
};
|
|
750
979
|
const [created] = await ctx.db.insert(table).values(data).returning();
|
|
980
|
+
await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
|
|
751
981
|
return created;
|
|
752
982
|
};
|
|
753
983
|
if (m.create) return m.create({
|
|
@@ -770,12 +1000,16 @@ function createCrudRouter(config) {
|
|
|
770
1000
|
message: "Resource not found or access denied"
|
|
771
1001
|
});
|
|
772
1002
|
const doUpdate = async (updateData) => {
|
|
1003
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1004
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
773
1005
|
const injectData = resolved.getInject(ctx, "update");
|
|
774
1006
|
const data = {
|
|
775
|
-
...
|
|
1007
|
+
...splitUpdate.data,
|
|
776
1008
|
...injectData
|
|
777
1009
|
};
|
|
778
|
-
|
|
1010
|
+
let updated = existing;
|
|
1011
|
+
if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
|
|
1012
|
+
await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
|
|
779
1013
|
return updated;
|
|
780
1014
|
};
|
|
781
1015
|
if (m.update) return m.update({
|
|
@@ -831,12 +1065,18 @@ function createCrudRouter(config) {
|
|
|
831
1065
|
await withGuard(ctx, "updateMany");
|
|
832
1066
|
const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
|
|
833
1067
|
const doUpdateMany = async (updateData) => {
|
|
1068
|
+
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1069
|
+
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
834
1070
|
const injectData = resolved.getInject(ctx, "update");
|
|
835
1071
|
const data = {
|
|
836
|
-
...
|
|
1072
|
+
...splitUpdate.data,
|
|
837
1073
|
...injectData
|
|
838
1074
|
};
|
|
839
|
-
|
|
1075
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1076
|
+
if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1077
|
+
else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1078
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
|
|
1079
|
+
return { updated: updated.length };
|
|
840
1080
|
};
|
|
841
1081
|
if (m.updateMany) return m.updateMany({
|
|
842
1082
|
ctx,
|
|
@@ -849,7 +1089,9 @@ function createCrudRouter(config) {
|
|
|
849
1089
|
upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
|
|
850
1090
|
await withGuard(ctx, "upsert");
|
|
851
1091
|
const doUpsert = async (inputData) => {
|
|
852
|
-
const
|
|
1092
|
+
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1093
|
+
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
1094
|
+
const inputId = splitInput.data[idField];
|
|
853
1095
|
let isNew = true;
|
|
854
1096
|
let existing = null;
|
|
855
1097
|
if (inputId) {
|
|
@@ -861,16 +1103,17 @@ function createCrudRouter(config) {
|
|
|
861
1103
|
}
|
|
862
1104
|
const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
|
|
863
1105
|
const data = {
|
|
864
|
-
...
|
|
1106
|
+
...splitInput.data,
|
|
865
1107
|
...injectData
|
|
866
1108
|
};
|
|
867
1109
|
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
868
1110
|
target: getIdColumn(),
|
|
869
1111
|
set: {
|
|
870
|
-
...
|
|
1112
|
+
...splitInput.data,
|
|
871
1113
|
...resolved.getInject(ctx, "update")
|
|
872
1114
|
}
|
|
873
1115
|
}).returning();
|
|
1116
|
+
await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
|
|
874
1117
|
return {
|
|
875
1118
|
data: result,
|
|
876
1119
|
isNew
|
|
@@ -886,22 +1129,32 @@ function createCrudRouter(config) {
|
|
|
886
1129
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
887
1130
|
await withGuard(ctx, "export");
|
|
888
1131
|
const doExport = async (exportInput$1) => {
|
|
889
|
-
const
|
|
890
|
-
|
|
1132
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1133
|
+
page: 1,
|
|
1134
|
+
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1135
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
1136
|
+
...exportInput$1
|
|
1137
|
+
});
|
|
1138
|
+
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1139
|
+
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
891
1140
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
892
1141
|
table,
|
|
893
1142
|
filters: validatedFilters,
|
|
894
|
-
joinOperator:
|
|
895
|
-
resolveColumn: (columnId) =>
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1143
|
+
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
1144
|
+
resolveColumn: (columnId) => {
|
|
1145
|
+
const id = String(columnId);
|
|
1146
|
+
if (id === idField) return getTableColumn(table, idField);
|
|
1147
|
+
return resolveColumnTarget({
|
|
1148
|
+
table,
|
|
1149
|
+
columnId: id,
|
|
1150
|
+
allowedColumns: filterableColumns
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
900
1153
|
}) : void 0);
|
|
901
1154
|
let query = ctx.db.select().from(table).$dynamic();
|
|
902
1155
|
if (where) query = query.where(where);
|
|
903
|
-
if (
|
|
904
|
-
const sortField =
|
|
1156
|
+
if (effectiveInput.sort?.length) {
|
|
1157
|
+
const sortField = effectiveInput.sort[0];
|
|
905
1158
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
906
1159
|
const column = resolveColumnTarget({
|
|
907
1160
|
table,
|
|
@@ -911,14 +1164,14 @@ function createCrudRouter(config) {
|
|
|
911
1164
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
912
1165
|
}
|
|
913
1166
|
}
|
|
914
|
-
const
|
|
1167
|
+
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
915
1168
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
916
1169
|
if (where) countQuery = countQuery.where(where);
|
|
917
1170
|
const total = (await countQuery)[0]?.count ?? 0;
|
|
918
1171
|
return {
|
|
919
|
-
data,
|
|
1172
|
+
data: enrichedData,
|
|
920
1173
|
total,
|
|
921
|
-
hasMore: total >
|
|
1174
|
+
hasMore: total > enrichedData.length
|
|
922
1175
|
};
|
|
923
1176
|
};
|
|
924
1177
|
const exportInput = input;
|
|
@@ -932,12 +1185,15 @@ function createCrudRouter(config) {
|
|
|
932
1185
|
createMany: resolved.procedureFactory("createMany").input(z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
|
|
933
1186
|
await withGuard(ctx, "createMany");
|
|
934
1187
|
const doCreateMany = async (items) => {
|
|
1188
|
+
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1189
|
+
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
935
1190
|
const injectData = resolved.getInject(ctx, "create");
|
|
936
|
-
const values =
|
|
937
|
-
...
|
|
1191
|
+
const values = splitItems.map(({ data }) => ({
|
|
1192
|
+
...data,
|
|
938
1193
|
...injectData
|
|
939
1194
|
}));
|
|
940
1195
|
const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1196
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
|
|
941
1197
|
return {
|
|
942
1198
|
created,
|
|
943
1199
|
count: created.length
|
|
@@ -958,8 +1214,10 @@ function createCrudRouter(config) {
|
|
|
958
1214
|
for (let i = 0; i < importData$1.rows.length; i++) {
|
|
959
1215
|
const row = importData$1.rows[i];
|
|
960
1216
|
const result = schema.safeParse(row);
|
|
961
|
-
if (result.success)
|
|
962
|
-
|
|
1217
|
+
if (result.success) {
|
|
1218
|
+
const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
|
|
1219
|
+
validRows.push(splitRow);
|
|
1220
|
+
} else {
|
|
963
1221
|
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
964
1222
|
failed.push({
|
|
965
1223
|
row: i,
|
|
@@ -979,26 +1237,30 @@ function createCrudRouter(config) {
|
|
|
979
1237
|
skipped: 0,
|
|
980
1238
|
failed
|
|
981
1239
|
};
|
|
1240
|
+
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
982
1241
|
const injectData = resolved.getInject(ctx, "create");
|
|
983
1242
|
const values = validRows.map((row) => ({
|
|
984
|
-
...row,
|
|
1243
|
+
...row.data,
|
|
985
1244
|
...injectData
|
|
986
1245
|
}));
|
|
987
1246
|
let insertedCount = 0;
|
|
988
1247
|
let updatedCount = 0;
|
|
989
1248
|
if (importData$1.onConflict === "upsert") {
|
|
990
1249
|
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]);
|
|
1250
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
992
1251
|
const setClause = { ...resolved.getInject(ctx, "update") };
|
|
993
1252
|
for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
|
|
994
1253
|
const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
|
|
995
1254
|
target: getIdColumn(),
|
|
996
1255
|
set: setClause
|
|
997
1256
|
}).returning({ id: getIdColumn() });
|
|
1257
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
998
1258
|
updatedCount = Math.min(existingCount, results.length);
|
|
999
1259
|
insertedCount = results.length - updatedCount;
|
|
1000
1260
|
} else {
|
|
1001
|
-
|
|
1261
|
+
const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1262
|
+
await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1263
|
+
insertedCount = inserted.length;
|
|
1002
1264
|
updatedCount = 0;
|
|
1003
1265
|
}
|
|
1004
1266
|
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.8",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -38,18 +38,18 @@
|
|
|
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",
|
|
52
|
+
"@internal/tsconfig": "0.1.0",
|
|
53
53
|
"@internal/tsdown-config": "0.1.0",
|
|
54
54
|
"@internal/vitest-config": "0.1.0"
|
|
55
55
|
},
|