@wordrhyme/auto-crud-server 1.3.1 → 1.4.0
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/README.md +48 -1
- package/dist/index.cjs +140 -91
- package/dist/index.d.cts +21 -6
- package/dist/index.d.ts +22 -7
- package/dist/index.js +138 -89
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -170,7 +170,8 @@ export { handler as GET, handler as POST };
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
未传 `sort` 或传空数组时,如果表存在 `createdAt` 且未被 `sortableColumns`
|
|
173
|
-
白名单排除,列表默认按 `createdAt`
|
|
173
|
+
白名单排除,列表默认按 `createdAt` 降序返回。存在有效排序时,服务端会自动追加
|
|
174
|
+
主键作为同方向的稳定排序条件,避免 OFFSET 分页在相同排序值之间漂移。
|
|
174
175
|
|
|
175
176
|
**输出**:
|
|
176
177
|
|
|
@@ -1120,6 +1121,50 @@ createCrudRouter({
|
|
|
1120
1121
|
|
|
1121
1122
|
---
|
|
1122
1123
|
|
|
1124
|
+
## ⚡ 性能与索引
|
|
1125
|
+
|
|
1126
|
+
- `list` 和 `export` 会并发执行数据查询与精确计数,降低一次数据库往返延迟。
|
|
1127
|
+
- 多字段 `sort` 会全部应用,并自动追加 `idField` 作为稳定 tie-breaker。
|
|
1128
|
+
- `perPage`、批量写入和导出都有上限,但深分页仍建议由业务 middleware 改为
|
|
1129
|
+
cursor/keyset pagination。
|
|
1130
|
+
- 不要开放所有字段过滤和排序;请通过 `filters`、`sort`、`search` 白名单只暴露
|
|
1131
|
+
已有索引支持的字段。
|
|
1132
|
+
|
|
1133
|
+
常见列表至少应覆盖默认排序;有租户和软删除时可使用部分复合索引:
|
|
1134
|
+
|
|
1135
|
+
```sql
|
|
1136
|
+
create index tasks_created_at_id_idx
|
|
1137
|
+
on tasks (created_at desc, id desc);
|
|
1138
|
+
|
|
1139
|
+
create index tasks_tenant_created_at_id_idx
|
|
1140
|
+
on tasks (tenant_id, created_at desc, id desc)
|
|
1141
|
+
where deleted_at is null;
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
`search` 的 contains 查询使用 `ILIKE '%term%'`。PostgreSQL 普通 B-tree 无法高效处理,
|
|
1145
|
+
应为真实文本列配置 `pg_trgm` GIN 索引;JSON/custom expression 需要与生成 SQL 一致的
|
|
1146
|
+
表达式索引。
|
|
1147
|
+
|
|
1148
|
+
扩展字段 provider 可以实现批量写接口,避免 `createMany`、`updateMany` 和 `import`
|
|
1149
|
+
逐行访问数据库。未实现时仍兼容旧的 `saveExtraValues`:
|
|
1150
|
+
|
|
1151
|
+
```typescript
|
|
1152
|
+
const extensions: CrudExtensionsProvider = {
|
|
1153
|
+
saveExtraValuesMany: async ({ id, rows, tx }) => {
|
|
1154
|
+
await saveExtensionRowsInOneStatement(tx, id, rows);
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
`createMany` 和 `import` 中携带扩展字段的每一行都必须提供唯一、稳定的 `idField`。
|
|
1160
|
+
服务端只按显式 ID 关联 `RETURNING` 结果,不依赖数据库未承诺的返回顺序;无法可靠映射时
|
|
1161
|
+
会在写入前返回 `PRECONDITION_FAILED`。
|
|
1162
|
+
|
|
1163
|
+
任意组合筛选的列表缓存失效成本较高。优先优化索引和查询计划;metadata 或低频变更的
|
|
1164
|
+
无筛选计数才适合按 TTL 缓存。
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1123
1168
|
## 📦 导出的类型
|
|
1124
1169
|
|
|
1125
1170
|
```typescript
|
|
@@ -1134,6 +1179,8 @@ export type {
|
|
|
1134
1179
|
CrudColumnConfig,
|
|
1135
1180
|
CrudColumnExpression,
|
|
1136
1181
|
CrudColumnRef,
|
|
1182
|
+
CrudExtensionValueWrite,
|
|
1183
|
+
CrudExtensionsProvider,
|
|
1137
1184
|
CrudRouterConfig,
|
|
1138
1185
|
CrudMiddleware,
|
|
1139
1186
|
GetInput,
|
package/dist/index.cjs
CHANGED
|
@@ -21,14 +21,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
|
-
let
|
|
25
|
-
|
|
24
|
+
let __trpc_server = require("@trpc/server");
|
|
25
|
+
__trpc_server = __toESM(__trpc_server);
|
|
26
26
|
let drizzle_orm = require("drizzle-orm");
|
|
27
27
|
drizzle_orm = __toESM(drizzle_orm);
|
|
28
28
|
let drizzle_zod = require("drizzle-zod");
|
|
29
29
|
drizzle_zod = __toESM(drizzle_zod);
|
|
30
|
-
let
|
|
31
|
-
|
|
30
|
+
let zod = require("zod");
|
|
31
|
+
zod = __toESM(zod);
|
|
32
32
|
let superjson = require("superjson");
|
|
33
33
|
superjson = __toESM(superjson);
|
|
34
34
|
let date_fns_addDays = require("date-fns/addDays");
|
|
@@ -876,24 +876,44 @@ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
|
876
876
|
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
877
877
|
};
|
|
878
878
|
}
|
|
879
|
-
|
|
879
|
+
function createCrudExtensionValueWrite(entityId, input) {
|
|
880
|
+
const { extraValues } = input;
|
|
881
|
+
if (!extraValues) return null;
|
|
882
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return null;
|
|
883
|
+
return {
|
|
884
|
+
entityId: String(entityId),
|
|
885
|
+
rawValues: input.rawValues,
|
|
886
|
+
baseValues: input.baseValues,
|
|
887
|
+
extraValues
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
async function persistCrudExtensionValueWrites(ctx, config, rows, preferBulk) {
|
|
880
891
|
const target = readCrudTarget(config);
|
|
881
|
-
|
|
882
|
-
if (!target || !extraValues) return;
|
|
883
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
892
|
+
if (!target || rows.length === 0) return;
|
|
884
893
|
const provider = resolveCrudExtensions(ctx, config);
|
|
885
|
-
if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
894
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new __trpc_server.TRPCError({
|
|
886
895
|
code: "PRECONDITION_FAILED",
|
|
887
896
|
message: "CRUD extension persistence is not available"
|
|
888
897
|
});
|
|
889
|
-
|
|
898
|
+
const tx = ctx?.tx;
|
|
899
|
+
if ((preferBulk || !provider.saveExtraValues) && provider.saveExtraValuesMany) {
|
|
900
|
+
await provider.saveExtraValuesMany({
|
|
901
|
+
id: target.id,
|
|
902
|
+
rows,
|
|
903
|
+
tx
|
|
904
|
+
});
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
await Promise.all(rows.map(async (row) => provider.saveExtraValues({
|
|
890
908
|
id: target.id,
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
909
|
+
...row,
|
|
910
|
+
tx
|
|
911
|
+
})));
|
|
912
|
+
}
|
|
913
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
914
|
+
const row = createCrudExtensionValueWrite(entityId, input);
|
|
915
|
+
if (!row) return;
|
|
916
|
+
await persistCrudExtensionValueWrites(ctx, config, [row], false);
|
|
897
917
|
}
|
|
898
918
|
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
899
919
|
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
@@ -905,7 +925,8 @@ async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
|
905
925
|
}
|
|
906
926
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
907
927
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
908
|
-
|
|
928
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
929
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new __trpc_server.TRPCError({
|
|
909
930
|
code: "PRECONDITION_FAILED",
|
|
910
931
|
message: "CRUD extension persistence is not available"
|
|
911
932
|
});
|
|
@@ -913,38 +934,44 @@ function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
|
913
934
|
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
914
935
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
915
936
|
}
|
|
937
|
+
function assertCrudExtraValuesMappableForRows(inputs, idField) {
|
|
938
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const input of inputs) {
|
|
940
|
+
const inputId = isObjectRecord(input.data) ? input.data[idField] : void 0;
|
|
941
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) {
|
|
942
|
+
const normalizedId = String(inputId);
|
|
943
|
+
idCounts.set(normalizedId, (idCounts.get(normalizedId) ?? 0) + 1);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const input of inputs) if (input.extraValues) {
|
|
947
|
+
const inputId = isObjectRecord(input.data) ? input.data[idField] : void 0;
|
|
948
|
+
if (inputId === null || inputId === void 0 || String(inputId).length === 0) throw new __trpc_server.TRPCError({
|
|
949
|
+
code: "PRECONDITION_FAILED",
|
|
950
|
+
message: "Batch CRUD extension writes require every row with extra values to provide a stable id."
|
|
951
|
+
});
|
|
952
|
+
const normalizedId = String(inputId);
|
|
953
|
+
if (idCounts.get(normalizedId) !== 1) throw new __trpc_server.TRPCError({
|
|
954
|
+
code: "PRECONDITION_FAILED",
|
|
955
|
+
message: `Batch CRUD extension writes require unique ids; duplicate id "${normalizedId}" was provided.`
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
916
959
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
960
|
+
if (inputs.filter((input) => input.extraValues).length === 0) return;
|
|
961
|
+
assertCrudExtraValuesMappableForRows(inputs, idField);
|
|
962
|
+
const inputById = /* @__PURE__ */ new Map();
|
|
920
963
|
for (const input of inputs) {
|
|
921
|
-
if (!
|
|
964
|
+
if (!isObjectRecord(input.data)) continue;
|
|
922
965
|
const inputId = input.data[idField];
|
|
923
|
-
if (inputId !== null && inputId !== void 0)
|
|
966
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) inputById.set(String(inputId), input);
|
|
924
967
|
}
|
|
925
|
-
|
|
926
|
-
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
927
|
-
if (!isObjectRecord(input.data)) return true;
|
|
928
|
-
const inputId = input.data[idField];
|
|
929
|
-
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
930
|
-
});
|
|
931
|
-
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new __trpc_server.TRPCError({
|
|
932
|
-
code: "PRECONDITION_FAILED",
|
|
933
|
-
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
934
|
-
});
|
|
935
|
-
await Promise.all(results.map((result, index) => {
|
|
968
|
+
await persistCrudExtensionValueWrites(ctx, config, results.flatMap((result) => {
|
|
936
969
|
const resultId = result[idField];
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new __trpc_server.TRPCError({
|
|
943
|
-
code: "PRECONDITION_FAILED",
|
|
944
|
-
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
945
|
-
});
|
|
946
|
-
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
947
|
-
}));
|
|
970
|
+
const input = resultId !== null && resultId !== void 0 ? inputById.get(String(resultId)) : void 0;
|
|
971
|
+
if (!input?.extraValues) return [];
|
|
972
|
+
const row = createCrudExtensionValueWrite(resultId, input);
|
|
973
|
+
return row ? [row] : [];
|
|
974
|
+
}), true);
|
|
948
975
|
}
|
|
949
976
|
function createCrudBatchTransactionInput(inputs) {
|
|
950
977
|
return {
|
|
@@ -1013,28 +1040,25 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1013
1040
|
filters: baseFilters
|
|
1014
1041
|
};
|
|
1015
1042
|
const provider = resolveCrudExtensions(ctx, config);
|
|
1016
|
-
const
|
|
1043
|
+
const matchRequests = [];
|
|
1017
1044
|
if (extensionFilters.length > 0) {
|
|
1018
1045
|
if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
|
|
1019
1046
|
code: "PRECONDITION_FAILED",
|
|
1020
1047
|
message: "CRUD extension filtering is not available"
|
|
1021
1048
|
});
|
|
1022
|
-
|
|
1049
|
+
matchRequests.push(provider.matchEntityIds({
|
|
1023
1050
|
id: target.id,
|
|
1024
1051
|
filters: extensionFilters,
|
|
1025
1052
|
joinOperator: input.joinOperator,
|
|
1026
1053
|
limit: 5e3
|
|
1027
|
-
});
|
|
1028
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1054
|
+
}));
|
|
1029
1055
|
}
|
|
1030
|
-
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1037
|
-
} else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
1056
|
+
if (search && !searchDisabled && provider?.searchEntityIds) matchRequests.push(provider.searchEntityIds({
|
|
1057
|
+
id: target.id,
|
|
1058
|
+
search,
|
|
1059
|
+
limit: 5e3
|
|
1060
|
+
}));
|
|
1061
|
+
else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
1038
1062
|
table,
|
|
1039
1063
|
search,
|
|
1040
1064
|
searchColumns
|
|
@@ -1042,6 +1066,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1042
1066
|
code: "PRECONDITION_FAILED",
|
|
1043
1067
|
message: "CRUD search is not available"
|
|
1044
1068
|
});
|
|
1069
|
+
const matchedSets = (await Promise.all(matchRequests)).map((matchedIds$1) => new Set(matchedIds$1));
|
|
1045
1070
|
if (matchedSets.length === 0) return {
|
|
1046
1071
|
...input,
|
|
1047
1072
|
filters: baseFilters
|
|
@@ -1062,11 +1087,17 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1062
1087
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
1063
1088
|
const term = typeof search === "string" ? search.trim() : "";
|
|
1064
1089
|
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
1065
|
-
const conditions = searchColumns.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1090
|
+
const conditions = searchColumns.flatMap((columnRef) => {
|
|
1091
|
+
const column = resolveColumnTarget({
|
|
1092
|
+
table,
|
|
1093
|
+
columnId: getColumnRefId(columnRef),
|
|
1094
|
+
allowedColumns: searchColumns
|
|
1095
|
+
});
|
|
1096
|
+
if (!column) return [];
|
|
1097
|
+
const pattern = `%${term}%`;
|
|
1098
|
+
const isJsonTextExpression = typeof columnRef !== "string" && columnRef.expression === void 0 && columnRef.jsonField !== void 0;
|
|
1099
|
+
return ["dataType" in column && column.dataType === "string" || isJsonTextExpression ? (0, drizzle_orm.ilike)(column, pattern) : (0, drizzle_orm.ilike)(drizzle_orm.sql`${column}::text`, pattern)];
|
|
1100
|
+
});
|
|
1070
1101
|
if (conditions.length === 0) return void 0;
|
|
1071
1102
|
if (conditions.length === 1) return conditions[0];
|
|
1072
1103
|
return (0, drizzle_orm.or)(...conditions);
|
|
@@ -1162,6 +1193,34 @@ function createCrudRouter(config) {
|
|
|
1162
1193
|
const getIdColumn = () => table[idField];
|
|
1163
1194
|
const tableColumnNames = getTableColumnNames(table);
|
|
1164
1195
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
1196
|
+
const buildOrderByExpressions = (requestedSort, useCreatedAtDefault) => {
|
|
1197
|
+
let sortFields = [];
|
|
1198
|
+
if (requestedSort && requestedSort.length > 0) sortFields = requestedSort;
|
|
1199
|
+
else if (useCreatedAtDefault && getTableColumn(table, "createdAt")) sortFields = [{
|
|
1200
|
+
id: "createdAt",
|
|
1201
|
+
desc: true
|
|
1202
|
+
}];
|
|
1203
|
+
const expressions = [];
|
|
1204
|
+
const appliedIds = /* @__PURE__ */ new Set();
|
|
1205
|
+
let tieBreakerDesc = false;
|
|
1206
|
+
for (const sortField of sortFields) if (!appliedIds.has(sortField.id) && validateColumn(sortField.id, sortableColumns)) {
|
|
1207
|
+
const column = resolveColumnTarget({
|
|
1208
|
+
table,
|
|
1209
|
+
columnId: sortField.id,
|
|
1210
|
+
allowedColumns: sortableColumns
|
|
1211
|
+
});
|
|
1212
|
+
if (column) {
|
|
1213
|
+
if (expressions.length === 0) tieBreakerDesc = sortField.desc;
|
|
1214
|
+
expressions.push(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
1215
|
+
appliedIds.add(sortField.id);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (expressions.length > 0 && !appliedIds.has(idField)) {
|
|
1219
|
+
const idColumn = getTableColumn(table, idField);
|
|
1220
|
+
if (idColumn) expressions.push(tieBreakerDesc ? (0, drizzle_orm.desc)(idColumn) : (0, drizzle_orm.asc)(idColumn));
|
|
1221
|
+
}
|
|
1222
|
+
return expressions;
|
|
1223
|
+
};
|
|
1165
1224
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
1166
1225
|
const conditions = [];
|
|
1167
1226
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
@@ -1235,24 +1294,15 @@ function createCrudRouter(config) {
|
|
|
1235
1294
|
})));
|
|
1236
1295
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1237
1296
|
if (where) query = query.where(where);
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
desc: true
|
|
1241
|
-
} : void 0);
|
|
1242
|
-
if (sortField) {
|
|
1243
|
-
if (validateColumn(sortField.id, sortableColumns)) {
|
|
1244
|
-
const column = resolveColumnTarget({
|
|
1245
|
-
table,
|
|
1246
|
-
columnId: sortField.id,
|
|
1247
|
-
allowedColumns: sortableColumns
|
|
1248
|
-
});
|
|
1249
|
-
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
1297
|
+
const orderByExpressions = buildOrderByExpressions(effectiveInput.sort, true);
|
|
1298
|
+
if (orderByExpressions.length > 0) query = query.orderBy(...orderByExpressions);
|
|
1253
1299
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
1254
1300
|
if (where) countQuery = countQuery.where(where);
|
|
1255
|
-
const
|
|
1301
|
+
const dataPromise = Promise.resolve(query.limit(effectiveInput.perPage).offset(offset));
|
|
1302
|
+
const countPromise = Promise.resolve(countQuery);
|
|
1303
|
+
const enrichedDataPromise = dataPromise.then(async (data) => enrichCrudRows(ctx, config, idField, data));
|
|
1304
|
+
const [enrichedData, countResult] = await Promise.all([enrichedDataPromise, countPromise]);
|
|
1305
|
+
const count = countResult[0]?.count ?? 0;
|
|
1256
1306
|
return {
|
|
1257
1307
|
data: enrichedData,
|
|
1258
1308
|
total: count,
|
|
@@ -1434,7 +1484,10 @@ function createCrudRouter(config) {
|
|
|
1434
1484
|
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1435
1485
|
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1436
1486
|
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1437
|
-
await
|
|
1487
|
+
await persistCrudExtensionValueWrites(writeCtx, config, updated.flatMap((row) => {
|
|
1488
|
+
const extensionRow = createCrudExtensionValueWrite(row[idField], splitUpdate);
|
|
1489
|
+
return extensionRow ? [extensionRow] : [];
|
|
1490
|
+
}), true);
|
|
1438
1491
|
return { updated: updated.length };
|
|
1439
1492
|
});
|
|
1440
1493
|
};
|
|
@@ -1530,21 +1583,15 @@ function createCrudRouter(config) {
|
|
|
1530
1583
|
})));
|
|
1531
1584
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1532
1585
|
if (where) query = query.where(where);
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
1536
|
-
const column = resolveColumnTarget({
|
|
1537
|
-
table,
|
|
1538
|
-
columnId: sortField.id,
|
|
1539
|
-
allowedColumns: sortableColumns
|
|
1540
|
-
});
|
|
1541
|
-
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
1586
|
+
const orderByExpressions = buildOrderByExpressions(effectiveInput.sort, false);
|
|
1587
|
+
if (orderByExpressions.length > 0) query = query.orderBy(...orderByExpressions);
|
|
1545
1588
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
1546
1589
|
if (where) countQuery = countQuery.where(where);
|
|
1547
|
-
const
|
|
1590
|
+
const dataPromise = Promise.resolve(query.limit(effectiveLimit));
|
|
1591
|
+
const countPromise = Promise.resolve(countQuery);
|
|
1592
|
+
const enrichedDataPromise = dataPromise.then(async (data) => enrichCrudRows(ctx, config, idField, data));
|
|
1593
|
+
const [enrichedData, countResult] = await Promise.all([enrichedDataPromise, countPromise]);
|
|
1594
|
+
const total = countResult[0]?.count ?? 0;
|
|
1548
1595
|
return {
|
|
1549
1596
|
data: enrichedData,
|
|
1550
1597
|
total,
|
|
@@ -1564,6 +1611,7 @@ function createCrudRouter(config) {
|
|
|
1564
1611
|
const doCreateMany = async (items) => {
|
|
1565
1612
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1566
1613
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1614
|
+
assertCrudExtraValuesMappableForRows(splitItems, idField);
|
|
1567
1615
|
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1568
1616
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1569
1617
|
const values = splitItems.map(({ data }) => ({
|
|
@@ -1617,6 +1665,7 @@ function createCrudRouter(config) {
|
|
|
1617
1665
|
failed
|
|
1618
1666
|
};
|
|
1619
1667
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1668
|
+
assertCrudExtraValuesMappableForRows(validRows, idField);
|
|
1620
1669
|
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1621
1670
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1622
1671
|
const values = validRows.map((row) => ({
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnyColumn, SQL, Table } from "drizzle-orm";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
import * as _trpc_server2 from "@trpc/server";
|
|
5
5
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
6
6
|
|
|
@@ -186,16 +186,31 @@ interface CrudExtensionFilter {
|
|
|
186
186
|
operator: string;
|
|
187
187
|
filterId?: string | undefined;
|
|
188
188
|
}
|
|
189
|
+
interface CrudExtensionValueWrite {
|
|
190
|
+
entityId: string;
|
|
191
|
+
rawValues: Record<string, unknown>;
|
|
192
|
+
baseValues: Record<string, unknown>;
|
|
193
|
+
extraValues: Record<string, unknown>;
|
|
194
|
+
}
|
|
189
195
|
interface CrudExtensionsProvider {
|
|
190
196
|
getMetadata?: (input: {
|
|
191
197
|
id: string;
|
|
192
198
|
}) => Promise<CrudExtensionMetadata>;
|
|
193
199
|
saveExtraValues?: (input: {
|
|
194
200
|
id: string;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
tx?: unknown;
|
|
202
|
+
} & CrudExtensionValueWrite) => Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Optional bulk persistence path used by createMany, updateMany, and import.
|
|
205
|
+
*
|
|
206
|
+
* Providers should implement this when extension values are stored in a
|
|
207
|
+
* database. The router falls back to saveExtraValues for backward
|
|
208
|
+
* compatibility when this method is absent. Batch create/import rows with
|
|
209
|
+
* extension values must provide stable, unique entity ids.
|
|
210
|
+
*/
|
|
211
|
+
saveExtraValuesMany?: (input: {
|
|
212
|
+
id: string;
|
|
213
|
+
rows: CrudExtensionValueWrite[];
|
|
199
214
|
tx?: unknown;
|
|
200
215
|
}) => Promise<void>;
|
|
201
216
|
readProjection?: (input: {
|
|
@@ -972,4 +987,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
972
987
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
973
988
|
type AppRouter = typeof appRouter;
|
|
974
989
|
//#endregion
|
|
975
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudCapabilityMetadata, type CrudColumnCapability, 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 DateFilterRange, type DateRangeResolver, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type ExtendedColumnFilter, type FilterOperator, type FilterVariant, 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 };
|
|
990
|
+
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudCapabilityMetadata, type CrudColumnCapability, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionValueWrite, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DateFilterRange, type DateRangeResolver, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type ExtendedColumnFilter, type FilterOperator, type FilterVariant, 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
|
-
import { z } from "zod";
|
|
2
|
-
import { AnyColumn, SQL, Table } from "drizzle-orm";
|
|
3
1
|
import * as _trpc_server2 from "@trpc/server";
|
|
2
|
+
import { AnyColumn, SQL, Table } from "drizzle-orm";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
6
6
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
@@ -187,16 +187,31 @@ interface CrudExtensionFilter {
|
|
|
187
187
|
operator: string;
|
|
188
188
|
filterId?: string | undefined;
|
|
189
189
|
}
|
|
190
|
+
interface CrudExtensionValueWrite {
|
|
191
|
+
entityId: string;
|
|
192
|
+
rawValues: Record<string, unknown>;
|
|
193
|
+
baseValues: Record<string, unknown>;
|
|
194
|
+
extraValues: Record<string, unknown>;
|
|
195
|
+
}
|
|
190
196
|
interface CrudExtensionsProvider {
|
|
191
197
|
getMetadata?: (input: {
|
|
192
198
|
id: string;
|
|
193
199
|
}) => Promise<CrudExtensionMetadata>;
|
|
194
200
|
saveExtraValues?: (input: {
|
|
195
201
|
id: string;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
tx?: unknown;
|
|
203
|
+
} & CrudExtensionValueWrite) => Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Optional bulk persistence path used by createMany, updateMany, and import.
|
|
206
|
+
*
|
|
207
|
+
* Providers should implement this when extension values are stored in a
|
|
208
|
+
* database. The router falls back to saveExtraValues for backward
|
|
209
|
+
* compatibility when this method is absent. Batch create/import rows with
|
|
210
|
+
* extension values must provide stable, unique entity ids.
|
|
211
|
+
*/
|
|
212
|
+
saveExtraValuesMany?: (input: {
|
|
213
|
+
id: string;
|
|
214
|
+
rows: CrudExtensionValueWrite[];
|
|
200
215
|
tx?: unknown;
|
|
201
216
|
}) => Promise<void>;
|
|
202
217
|
readProjection?: (input: {
|
|
@@ -973,4 +988,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
973
988
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
974
989
|
type AppRouter = typeof appRouter;
|
|
975
990
|
//#endregion
|
|
976
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudCapabilityMetadata, type CrudColumnCapability, 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 DateFilterRange, type DateRangeResolver, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type ExtendedColumnFilter, type FilterOperator, type FilterVariant, 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 };
|
|
991
|
+
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudCapabilityMetadata, type CrudColumnCapability, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionValueWrite, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DateFilterRange, type DateRangeResolver, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type ExtendedColumnFilter, type FilterOperator, type FilterVariant, 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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TRPCError, initTRPC } from "@trpc/server";
|
|
2
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
|
-
import {
|
|
4
|
+
import { z } from "zod";
|
|
5
5
|
import superjson from "superjson";
|
|
6
6
|
import { addDays } from "date-fns/addDays";
|
|
7
7
|
import { endOfDay } from "date-fns/endOfDay";
|
|
@@ -845,24 +845,44 @@ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
|
845
845
|
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
846
846
|
};
|
|
847
847
|
}
|
|
848
|
-
|
|
848
|
+
function createCrudExtensionValueWrite(entityId, input) {
|
|
849
|
+
const { extraValues } = input;
|
|
850
|
+
if (!extraValues) return null;
|
|
851
|
+
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return null;
|
|
852
|
+
return {
|
|
853
|
+
entityId: String(entityId),
|
|
854
|
+
rawValues: input.rawValues,
|
|
855
|
+
baseValues: input.baseValues,
|
|
856
|
+
extraValues
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function persistCrudExtensionValueWrites(ctx, config, rows, preferBulk) {
|
|
849
860
|
const target = readCrudTarget(config);
|
|
850
|
-
|
|
851
|
-
if (!target || !extraValues) return;
|
|
852
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
861
|
+
if (!target || rows.length === 0) return;
|
|
853
862
|
const provider = resolveCrudExtensions(ctx, config);
|
|
854
|
-
if (!provider?.saveExtraValues) throw new TRPCError({
|
|
863
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new TRPCError({
|
|
855
864
|
code: "PRECONDITION_FAILED",
|
|
856
865
|
message: "CRUD extension persistence is not available"
|
|
857
866
|
});
|
|
858
|
-
|
|
867
|
+
const tx = ctx?.tx;
|
|
868
|
+
if ((preferBulk || !provider.saveExtraValues) && provider.saveExtraValuesMany) {
|
|
869
|
+
await provider.saveExtraValuesMany({
|
|
870
|
+
id: target.id,
|
|
871
|
+
rows,
|
|
872
|
+
tx
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
await Promise.all(rows.map(async (row) => provider.saveExtraValues({
|
|
859
877
|
id: target.id,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
878
|
+
...row,
|
|
879
|
+
tx
|
|
880
|
+
})));
|
|
881
|
+
}
|
|
882
|
+
async function saveCrudExtraValues(ctx, config, entityId, input) {
|
|
883
|
+
const row = createCrudExtensionValueWrite(entityId, input);
|
|
884
|
+
if (!row) return;
|
|
885
|
+
await persistCrudExtensionValueWrites(ctx, config, [row], false);
|
|
866
886
|
}
|
|
867
887
|
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
868
888
|
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
@@ -874,7 +894,8 @@ async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
|
874
894
|
}
|
|
875
895
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
876
896
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
877
|
-
|
|
897
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
898
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new TRPCError({
|
|
878
899
|
code: "PRECONDITION_FAILED",
|
|
879
900
|
message: "CRUD extension persistence is not available"
|
|
880
901
|
});
|
|
@@ -882,38 +903,44 @@ function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
|
882
903
|
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
883
904
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
884
905
|
}
|
|
906
|
+
function assertCrudExtraValuesMappableForRows(inputs, idField) {
|
|
907
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const input of inputs) {
|
|
909
|
+
const inputId = isObjectRecord(input.data) ? input.data[idField] : void 0;
|
|
910
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) {
|
|
911
|
+
const normalizedId = String(inputId);
|
|
912
|
+
idCounts.set(normalizedId, (idCounts.get(normalizedId) ?? 0) + 1);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
for (const input of inputs) if (input.extraValues) {
|
|
916
|
+
const inputId = isObjectRecord(input.data) ? input.data[idField] : void 0;
|
|
917
|
+
if (inputId === null || inputId === void 0 || String(inputId).length === 0) throw new TRPCError({
|
|
918
|
+
code: "PRECONDITION_FAILED",
|
|
919
|
+
message: "Batch CRUD extension writes require every row with extra values to provide a stable id."
|
|
920
|
+
});
|
|
921
|
+
const normalizedId = String(inputId);
|
|
922
|
+
if (idCounts.get(normalizedId) !== 1) throw new TRPCError({
|
|
923
|
+
code: "PRECONDITION_FAILED",
|
|
924
|
+
message: `Batch CRUD extension writes require unique ids; duplicate id "${normalizedId}" was provided.`
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
885
928
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
929
|
+
if (inputs.filter((input) => input.extraValues).length === 0) return;
|
|
930
|
+
assertCrudExtraValuesMappableForRows(inputs, idField);
|
|
931
|
+
const inputById = /* @__PURE__ */ new Map();
|
|
889
932
|
for (const input of inputs) {
|
|
890
|
-
if (!
|
|
933
|
+
if (!isObjectRecord(input.data)) continue;
|
|
891
934
|
const inputId = input.data[idField];
|
|
892
|
-
if (inputId !== null && inputId !== void 0)
|
|
935
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) inputById.set(String(inputId), input);
|
|
893
936
|
}
|
|
894
|
-
|
|
895
|
-
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
896
|
-
if (!isObjectRecord(input.data)) return true;
|
|
897
|
-
const inputId = input.data[idField];
|
|
898
|
-
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
899
|
-
});
|
|
900
|
-
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new TRPCError({
|
|
901
|
-
code: "PRECONDITION_FAILED",
|
|
902
|
-
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
903
|
-
});
|
|
904
|
-
await Promise.all(results.map((result, index) => {
|
|
937
|
+
await persistCrudExtensionValueWrites(ctx, config, results.flatMap((result) => {
|
|
905
938
|
const resultId = result[idField];
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new TRPCError({
|
|
912
|
-
code: "PRECONDITION_FAILED",
|
|
913
|
-
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
914
|
-
});
|
|
915
|
-
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
916
|
-
}));
|
|
939
|
+
const input = resultId !== null && resultId !== void 0 ? inputById.get(String(resultId)) : void 0;
|
|
940
|
+
if (!input?.extraValues) return [];
|
|
941
|
+
const row = createCrudExtensionValueWrite(resultId, input);
|
|
942
|
+
return row ? [row] : [];
|
|
943
|
+
}), true);
|
|
917
944
|
}
|
|
918
945
|
function createCrudBatchTransactionInput(inputs) {
|
|
919
946
|
return {
|
|
@@ -982,28 +1009,25 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
982
1009
|
filters: baseFilters
|
|
983
1010
|
};
|
|
984
1011
|
const provider = resolveCrudExtensions(ctx, config);
|
|
985
|
-
const
|
|
1012
|
+
const matchRequests = [];
|
|
986
1013
|
if (extensionFilters.length > 0) {
|
|
987
1014
|
if (!provider?.matchEntityIds) throw new TRPCError({
|
|
988
1015
|
code: "PRECONDITION_FAILED",
|
|
989
1016
|
message: "CRUD extension filtering is not available"
|
|
990
1017
|
});
|
|
991
|
-
|
|
1018
|
+
matchRequests.push(provider.matchEntityIds({
|
|
992
1019
|
id: target.id,
|
|
993
1020
|
filters: extensionFilters,
|
|
994
1021
|
joinOperator: input.joinOperator,
|
|
995
1022
|
limit: 5e3
|
|
996
|
-
});
|
|
997
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1023
|
+
}));
|
|
998
1024
|
}
|
|
999
|
-
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1006
|
-
} else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
1025
|
+
if (search && !searchDisabled && provider?.searchEntityIds) matchRequests.push(provider.searchEntityIds({
|
|
1026
|
+
id: target.id,
|
|
1027
|
+
search,
|
|
1028
|
+
limit: 5e3
|
|
1029
|
+
}));
|
|
1030
|
+
else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
1007
1031
|
table,
|
|
1008
1032
|
search,
|
|
1009
1033
|
searchColumns
|
|
@@ -1011,6 +1035,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1011
1035
|
code: "PRECONDITION_FAILED",
|
|
1012
1036
|
message: "CRUD search is not available"
|
|
1013
1037
|
});
|
|
1038
|
+
const matchedSets = (await Promise.all(matchRequests)).map((matchedIds$1) => new Set(matchedIds$1));
|
|
1014
1039
|
if (matchedSets.length === 0) return {
|
|
1015
1040
|
...input,
|
|
1016
1041
|
filters: baseFilters
|
|
@@ -1031,11 +1056,17 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1031
1056
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
1032
1057
|
const term = typeof search === "string" ? search.trim() : "";
|
|
1033
1058
|
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
1034
|
-
const conditions = searchColumns.
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1059
|
+
const conditions = searchColumns.flatMap((columnRef) => {
|
|
1060
|
+
const column = resolveColumnTarget({
|
|
1061
|
+
table,
|
|
1062
|
+
columnId: getColumnRefId(columnRef),
|
|
1063
|
+
allowedColumns: searchColumns
|
|
1064
|
+
});
|
|
1065
|
+
if (!column) return [];
|
|
1066
|
+
const pattern = `%${term}%`;
|
|
1067
|
+
const isJsonTextExpression = typeof columnRef !== "string" && columnRef.expression === void 0 && columnRef.jsonField !== void 0;
|
|
1068
|
+
return ["dataType" in column && column.dataType === "string" || isJsonTextExpression ? ilike(column, pattern) : ilike(sql`${column}::text`, pattern)];
|
|
1069
|
+
});
|
|
1039
1070
|
if (conditions.length === 0) return void 0;
|
|
1040
1071
|
if (conditions.length === 1) return conditions[0];
|
|
1041
1072
|
return or(...conditions);
|
|
@@ -1131,6 +1162,34 @@ function createCrudRouter(config) {
|
|
|
1131
1162
|
const getIdColumn = () => table[idField];
|
|
1132
1163
|
const tableColumnNames = getTableColumnNames(table);
|
|
1133
1164
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
1165
|
+
const buildOrderByExpressions = (requestedSort, useCreatedAtDefault) => {
|
|
1166
|
+
let sortFields = [];
|
|
1167
|
+
if (requestedSort && requestedSort.length > 0) sortFields = requestedSort;
|
|
1168
|
+
else if (useCreatedAtDefault && getTableColumn(table, "createdAt")) sortFields = [{
|
|
1169
|
+
id: "createdAt",
|
|
1170
|
+
desc: true
|
|
1171
|
+
}];
|
|
1172
|
+
const expressions = [];
|
|
1173
|
+
const appliedIds = /* @__PURE__ */ new Set();
|
|
1174
|
+
let tieBreakerDesc = false;
|
|
1175
|
+
for (const sortField of sortFields) if (!appliedIds.has(sortField.id) && validateColumn(sortField.id, sortableColumns)) {
|
|
1176
|
+
const column = resolveColumnTarget({
|
|
1177
|
+
table,
|
|
1178
|
+
columnId: sortField.id,
|
|
1179
|
+
allowedColumns: sortableColumns
|
|
1180
|
+
});
|
|
1181
|
+
if (column) {
|
|
1182
|
+
if (expressions.length === 0) tieBreakerDesc = sortField.desc;
|
|
1183
|
+
expressions.push(sortField.desc ? desc(column) : asc(column));
|
|
1184
|
+
appliedIds.add(sortField.id);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (expressions.length > 0 && !appliedIds.has(idField)) {
|
|
1188
|
+
const idColumn = getTableColumn(table, idField);
|
|
1189
|
+
if (idColumn) expressions.push(tieBreakerDesc ? desc(idColumn) : asc(idColumn));
|
|
1190
|
+
}
|
|
1191
|
+
return expressions;
|
|
1192
|
+
};
|
|
1134
1193
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
1135
1194
|
const conditions = [];
|
|
1136
1195
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
@@ -1204,24 +1263,15 @@ function createCrudRouter(config) {
|
|
|
1204
1263
|
})));
|
|
1205
1264
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1206
1265
|
if (where) query = query.where(where);
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
desc: true
|
|
1210
|
-
} : void 0);
|
|
1211
|
-
if (sortField) {
|
|
1212
|
-
if (validateColumn(sortField.id, sortableColumns)) {
|
|
1213
|
-
const column = resolveColumnTarget({
|
|
1214
|
-
table,
|
|
1215
|
-
columnId: sortField.id,
|
|
1216
|
-
allowedColumns: sortableColumns
|
|
1217
|
-
});
|
|
1218
|
-
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
|
|
1266
|
+
const orderByExpressions = buildOrderByExpressions(effectiveInput.sort, true);
|
|
1267
|
+
if (orderByExpressions.length > 0) query = query.orderBy(...orderByExpressions);
|
|
1222
1268
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
1223
1269
|
if (where) countQuery = countQuery.where(where);
|
|
1224
|
-
const
|
|
1270
|
+
const dataPromise = Promise.resolve(query.limit(effectiveInput.perPage).offset(offset));
|
|
1271
|
+
const countPromise = Promise.resolve(countQuery);
|
|
1272
|
+
const enrichedDataPromise = dataPromise.then(async (data) => enrichCrudRows(ctx, config, idField, data));
|
|
1273
|
+
const [enrichedData, countResult] = await Promise.all([enrichedDataPromise, countPromise]);
|
|
1274
|
+
const count = countResult[0]?.count ?? 0;
|
|
1225
1275
|
return {
|
|
1226
1276
|
data: enrichedData,
|
|
1227
1277
|
total: count,
|
|
@@ -1403,7 +1453,10 @@ function createCrudRouter(config) {
|
|
|
1403
1453
|
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1404
1454
|
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1405
1455
|
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1406
|
-
await
|
|
1456
|
+
await persistCrudExtensionValueWrites(writeCtx, config, updated.flatMap((row) => {
|
|
1457
|
+
const extensionRow = createCrudExtensionValueWrite(row[idField], splitUpdate);
|
|
1458
|
+
return extensionRow ? [extensionRow] : [];
|
|
1459
|
+
}), true);
|
|
1407
1460
|
return { updated: updated.length };
|
|
1408
1461
|
});
|
|
1409
1462
|
};
|
|
@@ -1499,21 +1552,15 @@ function createCrudRouter(config) {
|
|
|
1499
1552
|
})));
|
|
1500
1553
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1501
1554
|
if (where) query = query.where(where);
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
1505
|
-
const column = resolveColumnTarget({
|
|
1506
|
-
table,
|
|
1507
|
-
columnId: sortField.id,
|
|
1508
|
-
allowedColumns: sortableColumns
|
|
1509
|
-
});
|
|
1510
|
-
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
|
|
1555
|
+
const orderByExpressions = buildOrderByExpressions(effectiveInput.sort, false);
|
|
1556
|
+
if (orderByExpressions.length > 0) query = query.orderBy(...orderByExpressions);
|
|
1514
1557
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
1515
1558
|
if (where) countQuery = countQuery.where(where);
|
|
1516
|
-
const
|
|
1559
|
+
const dataPromise = Promise.resolve(query.limit(effectiveLimit));
|
|
1560
|
+
const countPromise = Promise.resolve(countQuery);
|
|
1561
|
+
const enrichedDataPromise = dataPromise.then(async (data) => enrichCrudRows(ctx, config, idField, data));
|
|
1562
|
+
const [enrichedData, countResult] = await Promise.all([enrichedDataPromise, countPromise]);
|
|
1563
|
+
const total = countResult[0]?.count ?? 0;
|
|
1517
1564
|
return {
|
|
1518
1565
|
data: enrichedData,
|
|
1519
1566
|
total,
|
|
@@ -1533,6 +1580,7 @@ function createCrudRouter(config) {
|
|
|
1533
1580
|
const doCreateMany = async (items) => {
|
|
1534
1581
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1535
1582
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1583
|
+
assertCrudExtraValuesMappableForRows(splitItems, idField);
|
|
1536
1584
|
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1537
1585
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1538
1586
|
const values = splitItems.map(({ data }) => ({
|
|
@@ -1586,6 +1634,7 @@ function createCrudRouter(config) {
|
|
|
1586
1634
|
failed
|
|
1587
1635
|
};
|
|
1588
1636
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1637
|
+
assertCrudExtraValuesMappableForRows(validRows, idField);
|
|
1589
1638
|
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1590
1639
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1591
1640
|
const values = validRows.map((row) => ({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
49
|
"vitest": "^4.0.18",
|
|
50
50
|
"zod": "^4.3.6",
|
|
51
|
-
"@internal/eslint-config": "0.3.0",
|
|
52
51
|
"@internal/tsconfig": "0.1.0",
|
|
53
|
-
"@internal/
|
|
52
|
+
"@internal/eslint-config": "0.3.0",
|
|
54
53
|
"@internal/vitest-config": "0.1.0",
|
|
55
|
-
"@internal/prettier-config": "0.0.1"
|
|
54
|
+
"@internal/prettier-config": "0.0.1",
|
|
55
|
+
"@internal/tsdown-config": "0.1.0"
|
|
56
56
|
},
|
|
57
57
|
"prettier": "@internal/prettier-config",
|
|
58
58
|
"scripts": {
|