@wordrhyme/auto-crud-server 1.1.1 → 1.1.3
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 +238 -95
- package/dist/index.d.cts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +238 -95
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -648,6 +648,28 @@ function readCrudTarget(config) {
|
|
|
648
648
|
if (!id) return null;
|
|
649
649
|
return { id };
|
|
650
650
|
}
|
|
651
|
+
function buildCrudLifecycleHookId(config, operation) {
|
|
652
|
+
const target = readCrudTarget(config);
|
|
653
|
+
if (!target) return null;
|
|
654
|
+
return `${target.id}.${operation}`;
|
|
655
|
+
}
|
|
656
|
+
function getCrudLifecycleHooks(ctx) {
|
|
657
|
+
const hooks = ctx?.hooks;
|
|
658
|
+
return typeof hooks?.emit === "function" ? hooks : null;
|
|
659
|
+
}
|
|
660
|
+
function shouldWrapCrudLifecycleTransaction(ctx, config) {
|
|
661
|
+
return Boolean(buildCrudLifecycleHookId(config, "create") && getCrudLifecycleHooks(ctx));
|
|
662
|
+
}
|
|
663
|
+
async function emitCrudWriteLifecycle(ctx, config, operation, payload) {
|
|
664
|
+
const hookId = buildCrudLifecycleHookId(config, operation);
|
|
665
|
+
const hooks = getCrudLifecycleHooks(ctx);
|
|
666
|
+
if (!hookId || !hooks) return;
|
|
667
|
+
await hooks.emit(hookId, payload, {
|
|
668
|
+
dispatch: "hook",
|
|
669
|
+
mode: "effect",
|
|
670
|
+
tx: ctx?.tx
|
|
671
|
+
});
|
|
672
|
+
}
|
|
651
673
|
function resolveCrudExtensions(ctx, config) {
|
|
652
674
|
const extensions = config.extensions;
|
|
653
675
|
if (extensions === false) return null;
|
|
@@ -707,6 +729,14 @@ async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
|
707
729
|
tx: ctx?.tx
|
|
708
730
|
});
|
|
709
731
|
}
|
|
732
|
+
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
733
|
+
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
734
|
+
...ctx,
|
|
735
|
+
db: tx,
|
|
736
|
+
tx
|
|
737
|
+
}));
|
|
738
|
+
return operation(ctx);
|
|
739
|
+
}
|
|
710
740
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
711
741
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
712
742
|
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
@@ -718,6 +748,8 @@ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
|
718
748
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
719
749
|
}
|
|
720
750
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
751
|
+
const inputsWithExtra = inputs.filter((input) => input.extraValues);
|
|
752
|
+
if (inputsWithExtra.length === 0) return;
|
|
721
753
|
const extraByInputId = /* @__PURE__ */ new Map();
|
|
722
754
|
for (const input of inputs) {
|
|
723
755
|
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
@@ -725,15 +757,37 @@ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs)
|
|
|
725
757
|
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
726
758
|
}
|
|
727
759
|
const canUseIndexFallback = results.length === inputs.length;
|
|
760
|
+
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
761
|
+
if (!isObjectRecord(input.data)) return true;
|
|
762
|
+
const inputId = input.data[idField];
|
|
763
|
+
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
764
|
+
});
|
|
765
|
+
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new __trpc_server.TRPCError({
|
|
766
|
+
code: "PRECONDITION_FAILED",
|
|
767
|
+
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
768
|
+
});
|
|
728
769
|
await Promise.all(results.map((result, index) => {
|
|
729
770
|
const resultId = result[idField];
|
|
730
771
|
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
731
772
|
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
732
773
|
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
733
774
|
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
734
|
-
|
|
775
|
+
if (!input?.extraValues) return Promise.resolve();
|
|
776
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new __trpc_server.TRPCError({
|
|
777
|
+
code: "PRECONDITION_FAILED",
|
|
778
|
+
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
779
|
+
});
|
|
780
|
+
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
735
781
|
}));
|
|
736
782
|
}
|
|
783
|
+
function createCrudBatchTransactionInput(inputs) {
|
|
784
|
+
return {
|
|
785
|
+
data: {},
|
|
786
|
+
rawValues: {},
|
|
787
|
+
baseValues: {},
|
|
788
|
+
extraValues: inputs.some((input) => input.extraValues) ? {} : null
|
|
789
|
+
};
|
|
790
|
+
}
|
|
737
791
|
function readProjectionDisplay(value) {
|
|
738
792
|
if (!isObjectRecord(value)) return value;
|
|
739
793
|
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
@@ -772,7 +826,7 @@ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
|
772
826
|
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
773
827
|
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
774
828
|
}
|
|
775
|
-
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
829
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, input) {
|
|
776
830
|
const target = readCrudTarget(config);
|
|
777
831
|
const filters = input.filters ?? [];
|
|
778
832
|
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
@@ -808,7 +862,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
808
862
|
limit: 5e3
|
|
809
863
|
});
|
|
810
864
|
matchedSets.push(new Set(matchedIds$1));
|
|
811
|
-
}
|
|
865
|
+
} else if (search && !buildCrudSearchCondition({
|
|
866
|
+
table,
|
|
867
|
+
search,
|
|
868
|
+
searchColumns
|
|
869
|
+
})) throw new __trpc_server.TRPCError({
|
|
870
|
+
code: "PRECONDITION_FAILED",
|
|
871
|
+
message: "CRUD search is not available"
|
|
872
|
+
});
|
|
812
873
|
if (matchedSets.length === 0) return {
|
|
813
874
|
...input,
|
|
814
875
|
filters: baseFilters
|
|
@@ -826,6 +887,24 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
826
887
|
}]
|
|
827
888
|
};
|
|
828
889
|
}
|
|
890
|
+
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
891
|
+
const term = typeof search === "string" ? search.trim() : "";
|
|
892
|
+
if (!term || !searchColumns || searchColumns.length === 0) return void 0;
|
|
893
|
+
const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
|
|
894
|
+
table,
|
|
895
|
+
columnId: getColumnRefId(columnRef),
|
|
896
|
+
allowedColumns: searchColumns
|
|
897
|
+
})).filter((column) => column !== void 0).map((column) => drizzle_orm.sql`${column}::text ilike ${`%${term}%`}`);
|
|
898
|
+
if (conditions.length === 0) return void 0;
|
|
899
|
+
if (conditions.length === 1) return conditions[0];
|
|
900
|
+
return (0, drizzle_orm.or)(...conditions);
|
|
901
|
+
}
|
|
902
|
+
function combineConditions(...conditions) {
|
|
903
|
+
const filtered = conditions.filter((condition) => condition !== void 0);
|
|
904
|
+
if (filtered.length === 0) return void 0;
|
|
905
|
+
if (filtered.length === 1) return filtered[0];
|
|
906
|
+
return (0, drizzle_orm.and)(...filtered);
|
|
907
|
+
}
|
|
829
908
|
/**
|
|
830
909
|
* 创建 CRUD Router
|
|
831
910
|
*
|
|
@@ -839,7 +918,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
839
918
|
* @typeParam TUpdate - 更新输入类型
|
|
840
919
|
*/
|
|
841
920
|
function createCrudRouter(config) {
|
|
842
|
-
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
921
|
+
const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
843
922
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
844
923
|
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
845
924
|
const listOutputSchema = zod.z.object({
|
|
@@ -937,10 +1016,10 @@ function createCrudRouter(config) {
|
|
|
937
1016
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
938
1017
|
await withGuard(ctx, "list");
|
|
939
1018
|
const doList = async (listInput$1) => {
|
|
940
|
-
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
1019
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, listInput$1);
|
|
941
1020
|
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
942
1021
|
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
943
|
-
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
1022
|
+
const where = buildWhere(ctx, "list", combineConditions(validatedFilters?.length ? filterColumns({
|
|
944
1023
|
table,
|
|
945
1024
|
filters: validatedFilters,
|
|
946
1025
|
joinOperator: effectiveInput.joinOperator,
|
|
@@ -953,7 +1032,11 @@ function createCrudRouter(config) {
|
|
|
953
1032
|
allowedColumns: filterableColumns
|
|
954
1033
|
});
|
|
955
1034
|
}
|
|
956
|
-
}) : void 0
|
|
1035
|
+
}) : void 0, buildCrudSearchCondition({
|
|
1036
|
+
table,
|
|
1037
|
+
search: effectiveInput.search,
|
|
1038
|
+
searchColumns
|
|
1039
|
+
})));
|
|
957
1040
|
let query = ctx.db.select().from(table).$dynamic();
|
|
958
1041
|
if (where) query = query.where(where);
|
|
959
1042
|
if (effectiveInput.sort?.length) {
|
|
@@ -1012,14 +1095,24 @@ function createCrudRouter(config) {
|
|
|
1012
1095
|
const doCreate = async (inputData) => {
|
|
1013
1096
|
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
1014
1097
|
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1098
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitInput, async (writeCtx) => {
|
|
1099
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1100
|
+
const data = {
|
|
1101
|
+
...splitInput.data,
|
|
1102
|
+
...injectData
|
|
1103
|
+
};
|
|
1104
|
+
const [created] = await writeCtx.db.insert(table).values(data).returning();
|
|
1105
|
+
await saveCrudExtraValues(writeCtx, config, created?.[idField], splitInput);
|
|
1106
|
+
await emitCrudWriteLifecycle(writeCtx, config, "create", {
|
|
1107
|
+
input: inputData,
|
|
1108
|
+
row: created,
|
|
1109
|
+
entityId: created?.[idField],
|
|
1110
|
+
rawValues: splitInput.rawValues,
|
|
1111
|
+
baseValues: splitInput.baseValues,
|
|
1112
|
+
extraValues: splitInput.extraValues ?? {}
|
|
1113
|
+
});
|
|
1114
|
+
return created;
|
|
1115
|
+
});
|
|
1023
1116
|
};
|
|
1024
1117
|
if (m.create) return m.create({
|
|
1025
1118
|
ctx,
|
|
@@ -1043,15 +1136,27 @@ function createCrudRouter(config) {
|
|
|
1043
1136
|
const doUpdate = async (updateData) => {
|
|
1044
1137
|
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1045
1138
|
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1139
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitUpdate, async (writeCtx) => {
|
|
1140
|
+
const injectData = resolved.getInject(writeCtx, "update");
|
|
1141
|
+
const data = {
|
|
1142
|
+
...splitUpdate.data,
|
|
1143
|
+
...injectData
|
|
1144
|
+
};
|
|
1145
|
+
let updated = existing;
|
|
1146
|
+
if (Object.keys(data).length > 0) [updated] = await writeCtx.db.update(table).set(data).where(where).returning();
|
|
1147
|
+
await saveCrudExtraValues(writeCtx, config, input.id, splitUpdate);
|
|
1148
|
+
await emitCrudWriteLifecycle(writeCtx, config, "update", {
|
|
1149
|
+
id: input.id,
|
|
1150
|
+
input: updateData,
|
|
1151
|
+
row: updated,
|
|
1152
|
+
existing,
|
|
1153
|
+
entityId: input.id,
|
|
1154
|
+
rawValues: splitUpdate.rawValues,
|
|
1155
|
+
baseValues: splitUpdate.baseValues,
|
|
1156
|
+
extraValues: splitUpdate.extraValues ?? {}
|
|
1157
|
+
});
|
|
1158
|
+
return updated;
|
|
1159
|
+
});
|
|
1055
1160
|
};
|
|
1056
1161
|
if (m.update) return m.update({
|
|
1057
1162
|
ctx,
|
|
@@ -1072,10 +1177,23 @@ function createCrudRouter(config) {
|
|
|
1072
1177
|
message: "Resource not found or access denied"
|
|
1073
1178
|
});
|
|
1074
1179
|
const doDelete = async () => {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1180
|
+
return runCrudExtensionWriteTransaction(ctx, config, {
|
|
1181
|
+
data: {},
|
|
1182
|
+
rawValues: { id: input },
|
|
1183
|
+
baseValues: {},
|
|
1184
|
+
extraValues: null
|
|
1185
|
+
}, async (writeCtx) => {
|
|
1186
|
+
let deleted;
|
|
1187
|
+
if (softDelete) [deleted] = await writeCtx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
1188
|
+
else [deleted] = await writeCtx.db.delete(table).where(where).returning();
|
|
1189
|
+
await emitCrudWriteLifecycle(writeCtx, config, "delete", {
|
|
1190
|
+
id: input,
|
|
1191
|
+
row: deleted,
|
|
1192
|
+
existing,
|
|
1193
|
+
entityId: input
|
|
1194
|
+
});
|
|
1195
|
+
return deleted;
|
|
1196
|
+
});
|
|
1079
1197
|
};
|
|
1080
1198
|
if (m.delete) return m.delete({
|
|
1081
1199
|
ctx,
|
|
@@ -1108,16 +1226,18 @@ function createCrudRouter(config) {
|
|
|
1108
1226
|
const doUpdateMany = async (updateData) => {
|
|
1109
1227
|
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1110
1228
|
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1229
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitUpdate, async (writeCtx) => {
|
|
1230
|
+
const injectData = resolved.getInject(writeCtx, "update");
|
|
1231
|
+
const data = {
|
|
1232
|
+
...splitUpdate.data,
|
|
1233
|
+
...injectData
|
|
1234
|
+
};
|
|
1235
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1236
|
+
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1237
|
+
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1238
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(writeCtx, config, row[idField], splitUpdate)));
|
|
1239
|
+
return { updated: updated.length };
|
|
1240
|
+
});
|
|
1121
1241
|
};
|
|
1122
1242
|
if (m.updateMany) return m.updateMany({
|
|
1123
1243
|
ctx,
|
|
@@ -1142,23 +1262,35 @@ function createCrudRouter(config) {
|
|
|
1142
1262
|
isNew = !existing;
|
|
1143
1263
|
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
1144
1264
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
...injectData
|
|
1149
|
-
};
|
|
1150
|
-
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
1151
|
-
target: getIdColumn(),
|
|
1152
|
-
set: {
|
|
1265
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitInput, async (writeCtx) => {
|
|
1266
|
+
const injectData = resolved.getInject(writeCtx, isNew ? "create" : "update");
|
|
1267
|
+
const data = {
|
|
1153
1268
|
...splitInput.data,
|
|
1154
|
-
...
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1269
|
+
...injectData
|
|
1270
|
+
};
|
|
1271
|
+
const [result] = await writeCtx.db.insert(table).values(data).onConflictDoUpdate({
|
|
1272
|
+
target: getIdColumn(),
|
|
1273
|
+
set: {
|
|
1274
|
+
...splitInput.data,
|
|
1275
|
+
...resolved.getInject(writeCtx, "update")
|
|
1276
|
+
}
|
|
1277
|
+
}).returning();
|
|
1278
|
+
await saveCrudExtraValues(writeCtx, config, result?.[idField] ?? inputId, splitInput);
|
|
1279
|
+
await emitCrudWriteLifecycle(writeCtx, config, "upsert", {
|
|
1280
|
+
input: inputData,
|
|
1281
|
+
row: result,
|
|
1282
|
+
existing,
|
|
1283
|
+
isNew,
|
|
1284
|
+
entityId: result?.[idField] ?? inputId,
|
|
1285
|
+
rawValues: splitInput.rawValues,
|
|
1286
|
+
baseValues: splitInput.baseValues,
|
|
1287
|
+
extraValues: splitInput.extraValues ?? {}
|
|
1288
|
+
});
|
|
1289
|
+
return {
|
|
1290
|
+
data: result,
|
|
1291
|
+
isNew
|
|
1292
|
+
};
|
|
1293
|
+
});
|
|
1162
1294
|
};
|
|
1163
1295
|
if (m.upsert) return m.upsert({
|
|
1164
1296
|
ctx,
|
|
@@ -1170,7 +1302,7 @@ function createCrudRouter(config) {
|
|
|
1170
1302
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
1171
1303
|
await withGuard(ctx, "export");
|
|
1172
1304
|
const doExport = async (exportInput$1) => {
|
|
1173
|
-
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1305
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, {
|
|
1174
1306
|
page: 1,
|
|
1175
1307
|
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1176
1308
|
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
@@ -1178,7 +1310,7 @@ function createCrudRouter(config) {
|
|
|
1178
1310
|
});
|
|
1179
1311
|
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1180
1312
|
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
1181
|
-
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
1313
|
+
const where = buildWhere(ctx, "export", combineConditions(validatedFilters?.length ? filterColumns({
|
|
1182
1314
|
table,
|
|
1183
1315
|
filters: validatedFilters,
|
|
1184
1316
|
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
@@ -1191,7 +1323,11 @@ function createCrudRouter(config) {
|
|
|
1191
1323
|
allowedColumns: filterableColumns
|
|
1192
1324
|
});
|
|
1193
1325
|
}
|
|
1194
|
-
}) : void 0
|
|
1326
|
+
}) : void 0, buildCrudSearchCondition({
|
|
1327
|
+
table,
|
|
1328
|
+
search: effectiveInput.search,
|
|
1329
|
+
searchColumns
|
|
1330
|
+
})));
|
|
1195
1331
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1196
1332
|
if (where) query = query.where(where);
|
|
1197
1333
|
if (effectiveInput.sort?.length) {
|
|
@@ -1228,17 +1364,19 @@ function createCrudRouter(config) {
|
|
|
1228
1364
|
const doCreateMany = async (items) => {
|
|
1229
1365
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1230
1366
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1367
|
+
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1368
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1369
|
+
const values = splitItems.map(({ data }) => ({
|
|
1370
|
+
...data,
|
|
1371
|
+
...injectData
|
|
1372
|
+
}));
|
|
1373
|
+
const created = await writeCtx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1374
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, created, splitItems);
|
|
1375
|
+
return {
|
|
1376
|
+
created,
|
|
1377
|
+
count: created.length
|
|
1378
|
+
};
|
|
1379
|
+
});
|
|
1242
1380
|
};
|
|
1243
1381
|
if (m.createMany) return m.createMany({
|
|
1244
1382
|
ctx,
|
|
@@ -1279,36 +1417,41 @@ function createCrudRouter(config) {
|
|
|
1279
1417
|
failed
|
|
1280
1418
|
};
|
|
1281
1419
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1420
|
+
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1421
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1422
|
+
const values = validRows.map((row) => ({
|
|
1423
|
+
...row.data,
|
|
1424
|
+
...injectData
|
|
1425
|
+
}));
|
|
1426
|
+
let insertedCount$1 = 0;
|
|
1427
|
+
let updatedCount$1 = 0;
|
|
1428
|
+
if (importData$1.onConflict === "upsert") {
|
|
1429
|
+
const existingCount = (await writeCtx.db.select({ id: getIdColumn() }).from(table).where((0, drizzle_orm.inArray)(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
|
|
1430
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
1431
|
+
const setClause = { ...resolved.getInject(writeCtx, "update") };
|
|
1432
|
+
for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
|
|
1433
|
+
const results = await writeCtx.db.insert(table).values(values).onConflictDoUpdate({
|
|
1434
|
+
target: getIdColumn(),
|
|
1435
|
+
set: setClause
|
|
1436
|
+
}).returning({ id: getIdColumn() });
|
|
1437
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
1438
|
+
updatedCount$1 = Math.min(existingCount, results.length);
|
|
1439
|
+
insertedCount$1 = results.length - updatedCount$1;
|
|
1440
|
+
} else {
|
|
1441
|
+
const inserted = await writeCtx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1442
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1443
|
+
insertedCount$1 = inserted.length;
|
|
1444
|
+
updatedCount$1 = 0;
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
insertedCount: insertedCount$1,
|
|
1448
|
+
updatedCount: updatedCount$1
|
|
1449
|
+
};
|
|
1450
|
+
});
|
|
1308
1451
|
return {
|
|
1309
1452
|
success: insertedCount,
|
|
1310
1453
|
updated: updatedCount,
|
|
1311
|
-
skipped,
|
|
1454
|
+
skipped: validRows.length - insertedCount - updatedCount,
|
|
1312
1455
|
failed
|
|
1313
1456
|
};
|
|
1314
1457
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import * as
|
|
3
|
+
import * as _trpc_server0 from "@trpc/server";
|
|
4
4
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
5
5
|
import { AnyColumn, SQL } from "drizzle-orm";
|
|
6
6
|
|
|
@@ -12,13 +12,13 @@ import { AnyColumn, SQL } from "drizzle-orm";
|
|
|
12
12
|
interface Context {
|
|
13
13
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
14
14
|
}
|
|
15
|
-
declare const router:
|
|
15
|
+
declare const router: _trpc_server0.TRPCRouterBuilder<{
|
|
16
16
|
ctx: Context;
|
|
17
17
|
meta: object;
|
|
18
|
-
errorShape:
|
|
18
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
19
19
|
transformer: true;
|
|
20
20
|
}>;
|
|
21
|
-
declare const publicProcedure:
|
|
21
|
+
declare const publicProcedure: _trpc_server0.TRPCProcedureBuilder<Context, object, object, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, false>;
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/types/config.d.ts
|
|
24
24
|
type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
|
|
@@ -599,6 +599,17 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
599
599
|
* ]
|
|
600
600
|
*/
|
|
601
601
|
filterableColumns?: CrudColumnRef<TTable>[];
|
|
602
|
+
/**
|
|
603
|
+
* 全局搜索列白名单。
|
|
604
|
+
*
|
|
605
|
+
* 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
|
|
606
|
+
* 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
|
|
607
|
+
* `CrudExtensionsProvider.searchEntityIds` 处理。
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* searchColumns: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
|
|
611
|
+
*/
|
|
612
|
+
searchColumns?: CrudColumnRef<TTable>[];
|
|
602
613
|
/**
|
|
603
614
|
* 可排序的列白名单。
|
|
604
615
|
*
|
|
@@ -879,12 +890,12 @@ type CrudHookEventMap<PluginId extends string, ResourceName extends string, TCre
|
|
|
879
890
|
} } & { [K in `${PluginId}.${ResourceName}.createMany`]: TCreate[] };
|
|
880
891
|
//#endregion
|
|
881
892
|
//#region src/routers/index.d.ts
|
|
882
|
-
declare const appRouter:
|
|
893
|
+
declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
|
|
883
894
|
ctx: Context;
|
|
884
895
|
meta: object;
|
|
885
|
-
errorShape:
|
|
896
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
886
897
|
transformer: true;
|
|
887
|
-
},
|
|
898
|
+
}, _trpc_server0.TRPCDecorateCreateRouterOptions<{}>>;
|
|
888
899
|
type AppRouter = typeof appRouter;
|
|
889
900
|
//#endregion
|
|
890
901
|
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AnyColumn, SQL } from "drizzle-orm";
|
|
3
|
-
import * as
|
|
3
|
+
import * as _trpc_server2 from "@trpc/server";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
6
6
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
@@ -13,13 +13,13 @@ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
|
13
13
|
interface Context {
|
|
14
14
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
15
15
|
}
|
|
16
|
-
declare const router:
|
|
16
|
+
declare const router: _trpc_server2.TRPCRouterBuilder<{
|
|
17
17
|
ctx: Context;
|
|
18
18
|
meta: object;
|
|
19
|
-
errorShape:
|
|
19
|
+
errorShape: _trpc_server2.TRPCDefaultErrorShape;
|
|
20
20
|
transformer: true;
|
|
21
21
|
}>;
|
|
22
|
-
declare const publicProcedure:
|
|
22
|
+
declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/types/config.d.ts
|
|
25
25
|
type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
|
|
@@ -600,6 +600,17 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
600
600
|
* ]
|
|
601
601
|
*/
|
|
602
602
|
filterableColumns?: CrudColumnRef<TTable>[];
|
|
603
|
+
/**
|
|
604
|
+
* 全局搜索列白名单。
|
|
605
|
+
*
|
|
606
|
+
* 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
|
|
607
|
+
* 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
|
|
608
|
+
* `CrudExtensionsProvider.searchEntityIds` 处理。
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* searchColumns: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
|
|
612
|
+
*/
|
|
613
|
+
searchColumns?: CrudColumnRef<TTable>[];
|
|
603
614
|
/**
|
|
604
615
|
* 可排序的列白名单。
|
|
605
616
|
*
|
|
@@ -880,12 +891,12 @@ type CrudHookEventMap<PluginId extends string, ResourceName extends string, TCre
|
|
|
880
891
|
} } & { [K in `${PluginId}.${ResourceName}.createMany`]: TCreate[] };
|
|
881
892
|
//#endregion
|
|
882
893
|
//#region src/routers/index.d.ts
|
|
883
|
-
declare const appRouter:
|
|
894
|
+
declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
884
895
|
ctx: Context;
|
|
885
896
|
meta: object;
|
|
886
|
-
errorShape:
|
|
897
|
+
errorShape: _trpc_server2.TRPCDefaultErrorShape;
|
|
887
898
|
transformer: true;
|
|
888
|
-
},
|
|
899
|
+
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
889
900
|
type AppRouter = typeof appRouter;
|
|
890
901
|
//#endregion
|
|
891
902
|
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/dist/index.js
CHANGED
|
@@ -619,6 +619,28 @@ function readCrudTarget(config) {
|
|
|
619
619
|
if (!id) return null;
|
|
620
620
|
return { id };
|
|
621
621
|
}
|
|
622
|
+
function buildCrudLifecycleHookId(config, operation) {
|
|
623
|
+
const target = readCrudTarget(config);
|
|
624
|
+
if (!target) return null;
|
|
625
|
+
return `${target.id}.${operation}`;
|
|
626
|
+
}
|
|
627
|
+
function getCrudLifecycleHooks(ctx) {
|
|
628
|
+
const hooks = ctx?.hooks;
|
|
629
|
+
return typeof hooks?.emit === "function" ? hooks : null;
|
|
630
|
+
}
|
|
631
|
+
function shouldWrapCrudLifecycleTransaction(ctx, config) {
|
|
632
|
+
return Boolean(buildCrudLifecycleHookId(config, "create") && getCrudLifecycleHooks(ctx));
|
|
633
|
+
}
|
|
634
|
+
async function emitCrudWriteLifecycle(ctx, config, operation, payload) {
|
|
635
|
+
const hookId = buildCrudLifecycleHookId(config, operation);
|
|
636
|
+
const hooks = getCrudLifecycleHooks(ctx);
|
|
637
|
+
if (!hookId || !hooks) return;
|
|
638
|
+
await hooks.emit(hookId, payload, {
|
|
639
|
+
dispatch: "hook",
|
|
640
|
+
mode: "effect",
|
|
641
|
+
tx: ctx?.tx
|
|
642
|
+
});
|
|
643
|
+
}
|
|
622
644
|
function resolveCrudExtensions(ctx, config) {
|
|
623
645
|
const extensions = config.extensions;
|
|
624
646
|
if (extensions === false) return null;
|
|
@@ -678,6 +700,14 @@ async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
|
678
700
|
tx: ctx?.tx
|
|
679
701
|
});
|
|
680
702
|
}
|
|
703
|
+
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
704
|
+
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
705
|
+
...ctx,
|
|
706
|
+
db: tx,
|
|
707
|
+
tx
|
|
708
|
+
}));
|
|
709
|
+
return operation(ctx);
|
|
710
|
+
}
|
|
681
711
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
682
712
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
683
713
|
if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new TRPCError({
|
|
@@ -689,6 +719,8 @@ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
|
689
719
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
690
720
|
}
|
|
691
721
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
722
|
+
const inputsWithExtra = inputs.filter((input) => input.extraValues);
|
|
723
|
+
if (inputsWithExtra.length === 0) return;
|
|
692
724
|
const extraByInputId = /* @__PURE__ */ new Map();
|
|
693
725
|
for (const input of inputs) {
|
|
694
726
|
if (!input.extraValues || !isObjectRecord(input.data)) continue;
|
|
@@ -696,15 +728,37 @@ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs)
|
|
|
696
728
|
if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
|
|
697
729
|
}
|
|
698
730
|
const canUseIndexFallback = results.length === inputs.length;
|
|
731
|
+
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
732
|
+
if (!isObjectRecord(input.data)) return true;
|
|
733
|
+
const inputId = input.data[idField];
|
|
734
|
+
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
735
|
+
});
|
|
736
|
+
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new TRPCError({
|
|
737
|
+
code: "PRECONDITION_FAILED",
|
|
738
|
+
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
739
|
+
});
|
|
699
740
|
await Promise.all(results.map((result, index) => {
|
|
700
741
|
const resultId = result[idField];
|
|
701
742
|
const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
|
|
702
743
|
const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
|
|
703
744
|
const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
|
|
704
745
|
const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
|
|
705
|
-
|
|
746
|
+
if (!input?.extraValues) return Promise.resolve();
|
|
747
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new TRPCError({
|
|
748
|
+
code: "PRECONDITION_FAILED",
|
|
749
|
+
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
750
|
+
});
|
|
751
|
+
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
706
752
|
}));
|
|
707
753
|
}
|
|
754
|
+
function createCrudBatchTransactionInput(inputs) {
|
|
755
|
+
return {
|
|
756
|
+
data: {},
|
|
757
|
+
rawValues: {},
|
|
758
|
+
baseValues: {},
|
|
759
|
+
extraValues: inputs.some((input) => input.extraValues) ? {} : null
|
|
760
|
+
};
|
|
761
|
+
}
|
|
708
762
|
function readProjectionDisplay(value) {
|
|
709
763
|
if (!isObjectRecord(value)) return value;
|
|
710
764
|
if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
|
|
@@ -743,7 +797,7 @@ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
|
743
797
|
function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
|
|
744
798
|
return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
|
|
745
799
|
}
|
|
746
|
-
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
|
|
800
|
+
async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, input) {
|
|
747
801
|
const target = readCrudTarget(config);
|
|
748
802
|
const filters = input.filters ?? [];
|
|
749
803
|
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
@@ -779,7 +833,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
779
833
|
limit: 5e3
|
|
780
834
|
});
|
|
781
835
|
matchedSets.push(new Set(matchedIds$1));
|
|
782
|
-
}
|
|
836
|
+
} else if (search && !buildCrudSearchCondition({
|
|
837
|
+
table,
|
|
838
|
+
search,
|
|
839
|
+
searchColumns
|
|
840
|
+
})) throw new TRPCError({
|
|
841
|
+
code: "PRECONDITION_FAILED",
|
|
842
|
+
message: "CRUD search is not available"
|
|
843
|
+
});
|
|
783
844
|
if (matchedSets.length === 0) return {
|
|
784
845
|
...input,
|
|
785
846
|
filters: baseFilters
|
|
@@ -797,6 +858,24 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
797
858
|
}]
|
|
798
859
|
};
|
|
799
860
|
}
|
|
861
|
+
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
862
|
+
const term = typeof search === "string" ? search.trim() : "";
|
|
863
|
+
if (!term || !searchColumns || searchColumns.length === 0) return void 0;
|
|
864
|
+
const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
|
|
865
|
+
table,
|
|
866
|
+
columnId: getColumnRefId(columnRef),
|
|
867
|
+
allowedColumns: searchColumns
|
|
868
|
+
})).filter((column) => column !== void 0).map((column) => sql`${column}::text ilike ${`%${term}%`}`);
|
|
869
|
+
if (conditions.length === 0) return void 0;
|
|
870
|
+
if (conditions.length === 1) return conditions[0];
|
|
871
|
+
return or(...conditions);
|
|
872
|
+
}
|
|
873
|
+
function combineConditions(...conditions) {
|
|
874
|
+
const filtered = conditions.filter((condition) => condition !== void 0);
|
|
875
|
+
if (filtered.length === 0) return void 0;
|
|
876
|
+
if (filtered.length === 1) return filtered[0];
|
|
877
|
+
return and(...filtered);
|
|
878
|
+
}
|
|
800
879
|
/**
|
|
801
880
|
* 创建 CRUD Router
|
|
802
881
|
*
|
|
@@ -810,7 +889,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
810
889
|
* @typeParam TUpdate - 更新输入类型
|
|
811
890
|
*/
|
|
812
891
|
function createCrudRouter(config) {
|
|
813
|
-
const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
892
|
+
const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
814
893
|
const { selectSchema, schema, updateSchema } = resolveSchemas(config);
|
|
815
894
|
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
816
895
|
const listOutputSchema = z.object({
|
|
@@ -908,10 +987,10 @@ function createCrudRouter(config) {
|
|
|
908
987
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
909
988
|
await withGuard(ctx, "list");
|
|
910
989
|
const doList = async (listInput$1) => {
|
|
911
|
-
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
|
|
990
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, listInput$1);
|
|
912
991
|
const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
|
|
913
992
|
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
914
|
-
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
993
|
+
const where = buildWhere(ctx, "list", combineConditions(validatedFilters?.length ? filterColumns({
|
|
915
994
|
table,
|
|
916
995
|
filters: validatedFilters,
|
|
917
996
|
joinOperator: effectiveInput.joinOperator,
|
|
@@ -924,7 +1003,11 @@ function createCrudRouter(config) {
|
|
|
924
1003
|
allowedColumns: filterableColumns
|
|
925
1004
|
});
|
|
926
1005
|
}
|
|
927
|
-
}) : void 0
|
|
1006
|
+
}) : void 0, buildCrudSearchCondition({
|
|
1007
|
+
table,
|
|
1008
|
+
search: effectiveInput.search,
|
|
1009
|
+
searchColumns
|
|
1010
|
+
})));
|
|
928
1011
|
let query = ctx.db.select().from(table).$dynamic();
|
|
929
1012
|
if (where) query = query.where(where);
|
|
930
1013
|
if (effectiveInput.sort?.length) {
|
|
@@ -983,14 +1066,24 @@ function createCrudRouter(config) {
|
|
|
983
1066
|
const doCreate = async (inputData) => {
|
|
984
1067
|
const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
|
|
985
1068
|
assertCrudExtraValuesWritable(ctx, config, splitInput);
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1069
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitInput, async (writeCtx) => {
|
|
1070
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1071
|
+
const data = {
|
|
1072
|
+
...splitInput.data,
|
|
1073
|
+
...injectData
|
|
1074
|
+
};
|
|
1075
|
+
const [created] = await writeCtx.db.insert(table).values(data).returning();
|
|
1076
|
+
await saveCrudExtraValues(writeCtx, config, created?.[idField], splitInput);
|
|
1077
|
+
await emitCrudWriteLifecycle(writeCtx, config, "create", {
|
|
1078
|
+
input: inputData,
|
|
1079
|
+
row: created,
|
|
1080
|
+
entityId: created?.[idField],
|
|
1081
|
+
rawValues: splitInput.rawValues,
|
|
1082
|
+
baseValues: splitInput.baseValues,
|
|
1083
|
+
extraValues: splitInput.extraValues ?? {}
|
|
1084
|
+
});
|
|
1085
|
+
return created;
|
|
1086
|
+
});
|
|
994
1087
|
};
|
|
995
1088
|
if (m.create) return m.create({
|
|
996
1089
|
ctx,
|
|
@@ -1014,15 +1107,27 @@ function createCrudRouter(config) {
|
|
|
1014
1107
|
const doUpdate = async (updateData) => {
|
|
1015
1108
|
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1016
1109
|
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1110
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitUpdate, async (writeCtx) => {
|
|
1111
|
+
const injectData = resolved.getInject(writeCtx, "update");
|
|
1112
|
+
const data = {
|
|
1113
|
+
...splitUpdate.data,
|
|
1114
|
+
...injectData
|
|
1115
|
+
};
|
|
1116
|
+
let updated = existing;
|
|
1117
|
+
if (Object.keys(data).length > 0) [updated] = await writeCtx.db.update(table).set(data).where(where).returning();
|
|
1118
|
+
await saveCrudExtraValues(writeCtx, config, input.id, splitUpdate);
|
|
1119
|
+
await emitCrudWriteLifecycle(writeCtx, config, "update", {
|
|
1120
|
+
id: input.id,
|
|
1121
|
+
input: updateData,
|
|
1122
|
+
row: updated,
|
|
1123
|
+
existing,
|
|
1124
|
+
entityId: input.id,
|
|
1125
|
+
rawValues: splitUpdate.rawValues,
|
|
1126
|
+
baseValues: splitUpdate.baseValues,
|
|
1127
|
+
extraValues: splitUpdate.extraValues ?? {}
|
|
1128
|
+
});
|
|
1129
|
+
return updated;
|
|
1130
|
+
});
|
|
1026
1131
|
};
|
|
1027
1132
|
if (m.update) return m.update({
|
|
1028
1133
|
ctx,
|
|
@@ -1043,10 +1148,23 @@ function createCrudRouter(config) {
|
|
|
1043
1148
|
message: "Resource not found or access denied"
|
|
1044
1149
|
});
|
|
1045
1150
|
const doDelete = async () => {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1151
|
+
return runCrudExtensionWriteTransaction(ctx, config, {
|
|
1152
|
+
data: {},
|
|
1153
|
+
rawValues: { id: input },
|
|
1154
|
+
baseValues: {},
|
|
1155
|
+
extraValues: null
|
|
1156
|
+
}, async (writeCtx) => {
|
|
1157
|
+
let deleted;
|
|
1158
|
+
if (softDelete) [deleted] = await writeCtx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
|
|
1159
|
+
else [deleted] = await writeCtx.db.delete(table).where(where).returning();
|
|
1160
|
+
await emitCrudWriteLifecycle(writeCtx, config, "delete", {
|
|
1161
|
+
id: input,
|
|
1162
|
+
row: deleted,
|
|
1163
|
+
existing,
|
|
1164
|
+
entityId: input
|
|
1165
|
+
});
|
|
1166
|
+
return deleted;
|
|
1167
|
+
});
|
|
1050
1168
|
};
|
|
1051
1169
|
if (m.delete) return m.delete({
|
|
1052
1170
|
ctx,
|
|
@@ -1079,16 +1197,18 @@ function createCrudRouter(config) {
|
|
|
1079
1197
|
const doUpdateMany = async (updateData) => {
|
|
1080
1198
|
const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
|
|
1081
1199
|
assertCrudExtraValuesWritable(ctx, config, splitUpdate);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1200
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitUpdate, async (writeCtx) => {
|
|
1201
|
+
const injectData = resolved.getInject(writeCtx, "update");
|
|
1202
|
+
const data = {
|
|
1203
|
+
...splitUpdate.data,
|
|
1204
|
+
...injectData
|
|
1205
|
+
};
|
|
1206
|
+
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1207
|
+
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1208
|
+
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1209
|
+
await Promise.all(updated.map((row) => saveCrudExtraValues(writeCtx, config, row[idField], splitUpdate)));
|
|
1210
|
+
return { updated: updated.length };
|
|
1211
|
+
});
|
|
1092
1212
|
};
|
|
1093
1213
|
if (m.updateMany) return m.updateMany({
|
|
1094
1214
|
ctx,
|
|
@@ -1113,23 +1233,35 @@ function createCrudRouter(config) {
|
|
|
1113
1233
|
isNew = !existing;
|
|
1114
1234
|
if (!isNew && existing) await withAuthorize(ctx, existing, "update");
|
|
1115
1235
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
...injectData
|
|
1120
|
-
};
|
|
1121
|
-
const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
|
|
1122
|
-
target: getIdColumn(),
|
|
1123
|
-
set: {
|
|
1236
|
+
return runCrudExtensionWriteTransaction(ctx, config, splitInput, async (writeCtx) => {
|
|
1237
|
+
const injectData = resolved.getInject(writeCtx, isNew ? "create" : "update");
|
|
1238
|
+
const data = {
|
|
1124
1239
|
...splitInput.data,
|
|
1125
|
-
...
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1240
|
+
...injectData
|
|
1241
|
+
};
|
|
1242
|
+
const [result] = await writeCtx.db.insert(table).values(data).onConflictDoUpdate({
|
|
1243
|
+
target: getIdColumn(),
|
|
1244
|
+
set: {
|
|
1245
|
+
...splitInput.data,
|
|
1246
|
+
...resolved.getInject(writeCtx, "update")
|
|
1247
|
+
}
|
|
1248
|
+
}).returning();
|
|
1249
|
+
await saveCrudExtraValues(writeCtx, config, result?.[idField] ?? inputId, splitInput);
|
|
1250
|
+
await emitCrudWriteLifecycle(writeCtx, config, "upsert", {
|
|
1251
|
+
input: inputData,
|
|
1252
|
+
row: result,
|
|
1253
|
+
existing,
|
|
1254
|
+
isNew,
|
|
1255
|
+
entityId: result?.[idField] ?? inputId,
|
|
1256
|
+
rawValues: splitInput.rawValues,
|
|
1257
|
+
baseValues: splitInput.baseValues,
|
|
1258
|
+
extraValues: splitInput.extraValues ?? {}
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
data: result,
|
|
1262
|
+
isNew
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1133
1265
|
};
|
|
1134
1266
|
if (m.upsert) return m.upsert({
|
|
1135
1267
|
ctx,
|
|
@@ -1141,7 +1273,7 @@ function createCrudRouter(config) {
|
|
|
1141
1273
|
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
1142
1274
|
await withGuard(ctx, "export");
|
|
1143
1275
|
const doExport = async (exportInput$1) => {
|
|
1144
|
-
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
|
|
1276
|
+
const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, searchColumns, {
|
|
1145
1277
|
page: 1,
|
|
1146
1278
|
perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
|
|
1147
1279
|
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
@@ -1149,7 +1281,7 @@ function createCrudRouter(config) {
|
|
|
1149
1281
|
});
|
|
1150
1282
|
const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
|
|
1151
1283
|
const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
|
|
1152
|
-
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
1284
|
+
const where = buildWhere(ctx, "export", combineConditions(validatedFilters?.length ? filterColumns({
|
|
1153
1285
|
table,
|
|
1154
1286
|
filters: validatedFilters,
|
|
1155
1287
|
joinOperator: effectiveInput.joinOperator ?? "and",
|
|
@@ -1162,7 +1294,11 @@ function createCrudRouter(config) {
|
|
|
1162
1294
|
allowedColumns: filterableColumns
|
|
1163
1295
|
});
|
|
1164
1296
|
}
|
|
1165
|
-
}) : void 0
|
|
1297
|
+
}) : void 0, buildCrudSearchCondition({
|
|
1298
|
+
table,
|
|
1299
|
+
search: effectiveInput.search,
|
|
1300
|
+
searchColumns
|
|
1301
|
+
})));
|
|
1166
1302
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1167
1303
|
if (where) query = query.where(where);
|
|
1168
1304
|
if (effectiveInput.sort?.length) {
|
|
@@ -1199,17 +1335,19 @@ function createCrudRouter(config) {
|
|
|
1199
1335
|
const doCreateMany = async (items) => {
|
|
1200
1336
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1201
1337
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1338
|
+
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1339
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1340
|
+
const values = splitItems.map(({ data }) => ({
|
|
1341
|
+
...data,
|
|
1342
|
+
...injectData
|
|
1343
|
+
}));
|
|
1344
|
+
const created = await writeCtx.db.insert(table).values(values).onConflictDoNothing().returning();
|
|
1345
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, created, splitItems);
|
|
1346
|
+
return {
|
|
1347
|
+
created,
|
|
1348
|
+
count: created.length
|
|
1349
|
+
};
|
|
1350
|
+
});
|
|
1213
1351
|
};
|
|
1214
1352
|
if (m.createMany) return m.createMany({
|
|
1215
1353
|
ctx,
|
|
@@ -1250,36 +1388,41 @@ function createCrudRouter(config) {
|
|
|
1250
1388
|
failed
|
|
1251
1389
|
};
|
|
1252
1390
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1391
|
+
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1392
|
+
const injectData = resolved.getInject(writeCtx, "create");
|
|
1393
|
+
const values = validRows.map((row) => ({
|
|
1394
|
+
...row.data,
|
|
1395
|
+
...injectData
|
|
1396
|
+
}));
|
|
1397
|
+
let insertedCount$1 = 0;
|
|
1398
|
+
let updatedCount$1 = 0;
|
|
1399
|
+
if (importData$1.onConflict === "upsert") {
|
|
1400
|
+
const existingCount = (await writeCtx.db.select({ id: getIdColumn() }).from(table).where(inArray(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
|
|
1401
|
+
const updateFields = Object.keys(validRows[0].data);
|
|
1402
|
+
const setClause = { ...resolved.getInject(writeCtx, "update") };
|
|
1403
|
+
for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
|
|
1404
|
+
const results = await writeCtx.db.insert(table).values(values).onConflictDoUpdate({
|
|
1405
|
+
target: getIdColumn(),
|
|
1406
|
+
set: setClause
|
|
1407
|
+
}).returning({ id: getIdColumn() });
|
|
1408
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
|
|
1409
|
+
updatedCount$1 = Math.min(existingCount, results.length);
|
|
1410
|
+
insertedCount$1 = results.length - updatedCount$1;
|
|
1411
|
+
} else {
|
|
1412
|
+
const inserted = await writeCtx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
|
|
1413
|
+
await saveCrudExtraValuesForRows(writeCtx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
|
|
1414
|
+
insertedCount$1 = inserted.length;
|
|
1415
|
+
updatedCount$1 = 0;
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
insertedCount: insertedCount$1,
|
|
1419
|
+
updatedCount: updatedCount$1
|
|
1420
|
+
};
|
|
1421
|
+
});
|
|
1279
1422
|
return {
|
|
1280
1423
|
success: insertedCount,
|
|
1281
1424
|
updated: updatedCount,
|
|
1282
|
-
skipped,
|
|
1425
|
+
skipped: validRows.length - insertedCount - updatedCount,
|
|
1283
1426
|
failed
|
|
1284
1427
|
};
|
|
1285
1428
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.3",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"zod": "^3.0.0 || ^4.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
40
41
|
"@trpc/server": "^11.0.0",
|
|
41
42
|
"@types/node": "^22.19.17",
|
|
42
43
|
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
|
@@ -47,11 +48,11 @@
|
|
|
47
48
|
"typescript": "^5.9.3",
|
|
48
49
|
"vitest": "^4.0.18",
|
|
49
50
|
"zod": "^4.3.6",
|
|
50
|
-
"@internal/
|
|
51
|
-
"@internal/tsdown-config": "0.1.0",
|
|
51
|
+
"@internal/tsconfig": "0.1.0",
|
|
52
52
|
"@internal/prettier-config": "0.0.1",
|
|
53
|
+
"@internal/tsdown-config": "0.1.0",
|
|
53
54
|
"@internal/vitest-config": "0.1.0",
|
|
54
|
-
"@internal/
|
|
55
|
+
"@internal/eslint-config": "0.3.0"
|
|
55
56
|
},
|
|
56
57
|
"prettier": "@internal/prettier-config",
|
|
57
58
|
"scripts": {
|