@wordrhyme/auto-crud-server 1.3.0 → 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 +152 -99
- package/dist/index.d.cts +21 -6
- package/dist/index.d.ts +22 -7
- package/dist/index.js +141 -90
- 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,18 +21,22 @@ 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
|
-
let
|
|
35
|
-
|
|
34
|
+
let date_fns_addDays = require("date-fns/addDays");
|
|
35
|
+
date_fns_addDays = __toESM(date_fns_addDays);
|
|
36
|
+
let date_fns_endOfDay = require("date-fns/endOfDay");
|
|
37
|
+
date_fns_endOfDay = __toESM(date_fns_endOfDay);
|
|
38
|
+
let date_fns_startOfDay = require("date-fns/startOfDay");
|
|
39
|
+
date_fns_startOfDay = __toESM(date_fns_startOfDay);
|
|
36
40
|
|
|
37
41
|
//#region src/trpc.ts
|
|
38
42
|
const t = __trpc_server.initTRPC.context().create({ transformer: superjson.default });
|
|
@@ -256,16 +260,16 @@ function filterColumns({ table, filters, joinOperator, resolveColumn, resolveDat
|
|
|
256
260
|
if (!amount || !unit) return void 0;
|
|
257
261
|
switch (unit) {
|
|
258
262
|
case "days":
|
|
259
|
-
startDate = (0,
|
|
260
|
-
endDate = (0,
|
|
263
|
+
startDate = (0, date_fns_startOfDay.startOfDay)((0, date_fns_addDays.addDays)(today, Number.parseInt(amount, 10)));
|
|
264
|
+
endDate = (0, date_fns_endOfDay.endOfDay)(startDate);
|
|
261
265
|
break;
|
|
262
266
|
case "weeks":
|
|
263
|
-
startDate = (0,
|
|
264
|
-
endDate = (0,
|
|
267
|
+
startDate = (0, date_fns_startOfDay.startOfDay)((0, date_fns_addDays.addDays)(today, Number.parseInt(amount, 10) * 7));
|
|
268
|
+
endDate = (0, date_fns_endOfDay.endOfDay)((0, date_fns_addDays.addDays)(startDate, 6));
|
|
265
269
|
break;
|
|
266
270
|
case "months":
|
|
267
|
-
startDate = (0,
|
|
268
|
-
endDate = (0,
|
|
271
|
+
startDate = (0, date_fns_startOfDay.startOfDay)((0, date_fns_addDays.addDays)(today, Number.parseInt(amount, 10) * 30));
|
|
272
|
+
endDate = (0, date_fns_endOfDay.endOfDay)((0, date_fns_addDays.addDays)(startDate, 29));
|
|
269
273
|
break;
|
|
270
274
|
default: return;
|
|
271
275
|
}
|
|
@@ -872,24 +876,44 @@ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
|
872
876
|
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
873
877
|
};
|
|
874
878
|
}
|
|
875
|
-
|
|
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) {
|
|
876
891
|
const target = readCrudTarget(config);
|
|
877
|
-
|
|
878
|
-
if (!target || !extraValues) return;
|
|
879
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
892
|
+
if (!target || rows.length === 0) return;
|
|
880
893
|
const provider = resolveCrudExtensions(ctx, config);
|
|
881
|
-
if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
|
|
894
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new __trpc_server.TRPCError({
|
|
882
895
|
code: "PRECONDITION_FAILED",
|
|
883
896
|
message: "CRUD extension persistence is not available"
|
|
884
897
|
});
|
|
885
|
-
|
|
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({
|
|
886
908
|
id: target.id,
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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);
|
|
893
917
|
}
|
|
894
918
|
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
895
919
|
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
@@ -901,7 +925,8 @@ async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
|
901
925
|
}
|
|
902
926
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
903
927
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
904
|
-
|
|
928
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
929
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new __trpc_server.TRPCError({
|
|
905
930
|
code: "PRECONDITION_FAILED",
|
|
906
931
|
message: "CRUD extension persistence is not available"
|
|
907
932
|
});
|
|
@@ -909,38 +934,44 @@ function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
|
909
934
|
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
910
935
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
911
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
|
+
}
|
|
912
959
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const
|
|
960
|
+
if (inputs.filter((input) => input.extraValues).length === 0) return;
|
|
961
|
+
assertCrudExtraValuesMappableForRows(inputs, idField);
|
|
962
|
+
const inputById = /* @__PURE__ */ new Map();
|
|
916
963
|
for (const input of inputs) {
|
|
917
|
-
if (!
|
|
964
|
+
if (!isObjectRecord(input.data)) continue;
|
|
918
965
|
const inputId = input.data[idField];
|
|
919
|
-
if (inputId !== null && inputId !== void 0)
|
|
966
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) inputById.set(String(inputId), input);
|
|
920
967
|
}
|
|
921
|
-
|
|
922
|
-
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
923
|
-
if (!isObjectRecord(input.data)) return true;
|
|
924
|
-
const inputId = input.data[idField];
|
|
925
|
-
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
926
|
-
});
|
|
927
|
-
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new __trpc_server.TRPCError({
|
|
928
|
-
code: "PRECONDITION_FAILED",
|
|
929
|
-
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
930
|
-
});
|
|
931
|
-
await Promise.all(results.map((result, index) => {
|
|
968
|
+
await persistCrudExtensionValueWrites(ctx, config, results.flatMap((result) => {
|
|
932
969
|
const resultId = result[idField];
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new __trpc_server.TRPCError({
|
|
939
|
-
code: "PRECONDITION_FAILED",
|
|
940
|
-
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
941
|
-
});
|
|
942
|
-
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
943
|
-
}));
|
|
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);
|
|
944
975
|
}
|
|
945
976
|
function createCrudBatchTransactionInput(inputs) {
|
|
946
977
|
return {
|
|
@@ -1009,28 +1040,25 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1009
1040
|
filters: baseFilters
|
|
1010
1041
|
};
|
|
1011
1042
|
const provider = resolveCrudExtensions(ctx, config);
|
|
1012
|
-
const
|
|
1043
|
+
const matchRequests = [];
|
|
1013
1044
|
if (extensionFilters.length > 0) {
|
|
1014
1045
|
if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
|
|
1015
1046
|
code: "PRECONDITION_FAILED",
|
|
1016
1047
|
message: "CRUD extension filtering is not available"
|
|
1017
1048
|
});
|
|
1018
|
-
|
|
1049
|
+
matchRequests.push(provider.matchEntityIds({
|
|
1019
1050
|
id: target.id,
|
|
1020
1051
|
filters: extensionFilters,
|
|
1021
1052
|
joinOperator: input.joinOperator,
|
|
1022
1053
|
limit: 5e3
|
|
1023
|
-
});
|
|
1024
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1054
|
+
}));
|
|
1025
1055
|
}
|
|
1026
|
-
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1033
|
-
} 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({
|
|
1034
1062
|
table,
|
|
1035
1063
|
search,
|
|
1036
1064
|
searchColumns
|
|
@@ -1038,6 +1066,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1038
1066
|
code: "PRECONDITION_FAILED",
|
|
1039
1067
|
message: "CRUD search is not available"
|
|
1040
1068
|
});
|
|
1069
|
+
const matchedSets = (await Promise.all(matchRequests)).map((matchedIds$1) => new Set(matchedIds$1));
|
|
1041
1070
|
if (matchedSets.length === 0) return {
|
|
1042
1071
|
...input,
|
|
1043
1072
|
filters: baseFilters
|
|
@@ -1058,11 +1087,17 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1058
1087
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
1059
1088
|
const term = typeof search === "string" ? search.trim() : "";
|
|
1060
1089
|
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
1061
|
-
const conditions = searchColumns.
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
});
|
|
1066
1101
|
if (conditions.length === 0) return void 0;
|
|
1067
1102
|
if (conditions.length === 1) return conditions[0];
|
|
1068
1103
|
return (0, drizzle_orm.or)(...conditions);
|
|
@@ -1158,6 +1193,34 @@ function createCrudRouter(config) {
|
|
|
1158
1193
|
const getIdColumn = () => table[idField];
|
|
1159
1194
|
const tableColumnNames = getTableColumnNames(table);
|
|
1160
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
|
+
};
|
|
1161
1224
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
1162
1225
|
const conditions = [];
|
|
1163
1226
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
@@ -1231,24 +1294,15 @@ function createCrudRouter(config) {
|
|
|
1231
1294
|
})));
|
|
1232
1295
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1233
1296
|
if (where) query = query.where(where);
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
desc: true
|
|
1237
|
-
} : void 0);
|
|
1238
|
-
if (sortField) {
|
|
1239
|
-
if (validateColumn(sortField.id, sortableColumns)) {
|
|
1240
|
-
const column = resolveColumnTarget({
|
|
1241
|
-
table,
|
|
1242
|
-
columnId: sortField.id,
|
|
1243
|
-
allowedColumns: sortableColumns
|
|
1244
|
-
});
|
|
1245
|
-
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
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);
|
|
1249
1299
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
1250
1300
|
if (where) countQuery = countQuery.where(where);
|
|
1251
|
-
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;
|
|
1252
1306
|
return {
|
|
1253
1307
|
data: enrichedData,
|
|
1254
1308
|
total: count,
|
|
@@ -1430,7 +1484,10 @@ function createCrudRouter(config) {
|
|
|
1430
1484
|
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1431
1485
|
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1432
1486
|
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1433
|
-
await
|
|
1487
|
+
await persistCrudExtensionValueWrites(writeCtx, config, updated.flatMap((row) => {
|
|
1488
|
+
const extensionRow = createCrudExtensionValueWrite(row[idField], splitUpdate);
|
|
1489
|
+
return extensionRow ? [extensionRow] : [];
|
|
1490
|
+
}), true);
|
|
1434
1491
|
return { updated: updated.length };
|
|
1435
1492
|
});
|
|
1436
1493
|
};
|
|
@@ -1526,21 +1583,15 @@ function createCrudRouter(config) {
|
|
|
1526
1583
|
})));
|
|
1527
1584
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1528
1585
|
if (where) query = query.where(where);
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
1532
|
-
const column = resolveColumnTarget({
|
|
1533
|
-
table,
|
|
1534
|
-
columnId: sortField.id,
|
|
1535
|
-
allowedColumns: sortableColumns
|
|
1536
|
-
});
|
|
1537
|
-
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
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);
|
|
1541
1588
|
let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
|
|
1542
1589
|
if (where) countQuery = countQuery.where(where);
|
|
1543
|
-
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;
|
|
1544
1595
|
return {
|
|
1545
1596
|
data: enrichedData,
|
|
1546
1597
|
total,
|
|
@@ -1560,6 +1611,7 @@ function createCrudRouter(config) {
|
|
|
1560
1611
|
const doCreateMany = async (items) => {
|
|
1561
1612
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1562
1613
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1614
|
+
assertCrudExtraValuesMappableForRows(splitItems, idField);
|
|
1563
1615
|
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1564
1616
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1565
1617
|
const values = splitItems.map(({ data }) => ({
|
|
@@ -1613,6 +1665,7 @@ function createCrudRouter(config) {
|
|
|
1613
1665
|
failed
|
|
1614
1666
|
};
|
|
1615
1667
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1668
|
+
assertCrudExtraValuesMappableForRows(validRows, idField);
|
|
1616
1669
|
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1617
1670
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1618
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,9 +1,11 @@
|
|
|
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
|
-
import { addDays
|
|
6
|
+
import { addDays } from "date-fns/addDays";
|
|
7
|
+
import { endOfDay } from "date-fns/endOfDay";
|
|
8
|
+
import { startOfDay } from "date-fns/startOfDay";
|
|
7
9
|
|
|
8
10
|
//#region src/trpc.ts
|
|
9
11
|
const t = initTRPC.context().create({ transformer: superjson });
|
|
@@ -843,24 +845,44 @@ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
|
|
|
843
845
|
extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
|
|
844
846
|
};
|
|
845
847
|
}
|
|
846
|
-
|
|
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) {
|
|
847
860
|
const target = readCrudTarget(config);
|
|
848
|
-
|
|
849
|
-
if (!target || !extraValues) return;
|
|
850
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
|
|
861
|
+
if (!target || rows.length === 0) return;
|
|
851
862
|
const provider = resolveCrudExtensions(ctx, config);
|
|
852
|
-
if (!provider?.saveExtraValues) throw new TRPCError({
|
|
863
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new TRPCError({
|
|
853
864
|
code: "PRECONDITION_FAILED",
|
|
854
865
|
message: "CRUD extension persistence is not available"
|
|
855
866
|
});
|
|
856
|
-
|
|
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({
|
|
857
877
|
id: target.id,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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);
|
|
864
886
|
}
|
|
865
887
|
async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
866
888
|
if ((input.extraValues || shouldWrapCrudLifecycleTransaction(ctx, config)) && ctx.tx === void 0 && typeof ctx.db?.transaction === "function") return ctx.db.transaction((tx) => operation({
|
|
@@ -872,7 +894,8 @@ async function runCrudExtensionWriteTransaction(ctx, config, input, operation) {
|
|
|
872
894
|
}
|
|
873
895
|
function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
874
896
|
if (!readCrudTarget(config) || !input.extraValues) return;
|
|
875
|
-
|
|
897
|
+
const provider = resolveCrudExtensions(ctx, config);
|
|
898
|
+
if (!provider?.saveExtraValues && !provider?.saveExtraValuesMany) throw new TRPCError({
|
|
876
899
|
code: "PRECONDITION_FAILED",
|
|
877
900
|
message: "CRUD extension persistence is not available"
|
|
878
901
|
});
|
|
@@ -880,38 +903,44 @@ function assertCrudExtraValuesWritable(ctx, config, input) {
|
|
|
880
903
|
function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
|
|
881
904
|
if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
|
|
882
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
|
+
}
|
|
883
928
|
async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const
|
|
929
|
+
if (inputs.filter((input) => input.extraValues).length === 0) return;
|
|
930
|
+
assertCrudExtraValuesMappableForRows(inputs, idField);
|
|
931
|
+
const inputById = /* @__PURE__ */ new Map();
|
|
887
932
|
for (const input of inputs) {
|
|
888
|
-
if (!
|
|
933
|
+
if (!isObjectRecord(input.data)) continue;
|
|
889
934
|
const inputId = input.data[idField];
|
|
890
|
-
if (inputId !== null && inputId !== void 0)
|
|
935
|
+
if (inputId !== null && inputId !== void 0 && String(inputId).length > 0) inputById.set(String(inputId), input);
|
|
891
936
|
}
|
|
892
|
-
|
|
893
|
-
const hasUnidentifiedExtraInput = inputsWithExtra.some((input) => {
|
|
894
|
-
if (!isObjectRecord(input.data)) return true;
|
|
895
|
-
const inputId = input.data[idField];
|
|
896
|
-
return inputId === null || inputId === void 0 || String(inputId).length === 0;
|
|
897
|
-
});
|
|
898
|
-
if (!canUseIndexFallback && hasUnidentifiedExtraInput) throw new TRPCError({
|
|
899
|
-
code: "PRECONDITION_FAILED",
|
|
900
|
-
message: "Cannot persist CRUD extension values because inserted rows cannot be matched to input rows. Provide stable ids or avoid partial-conflict batch writes."
|
|
901
|
-
});
|
|
902
|
-
await Promise.all(results.map((result, index) => {
|
|
937
|
+
await persistCrudExtensionValueWrites(ctx, config, results.flatMap((result) => {
|
|
903
938
|
const resultId = result[idField];
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (entityId === null || entityId === void 0 || String(entityId).length === 0) throw new TRPCError({
|
|
910
|
-
code: "PRECONDITION_FAILED",
|
|
911
|
-
message: "Cannot persist CRUD extension values because the created entity id is unavailable."
|
|
912
|
-
});
|
|
913
|
-
return saveCrudExtraValues(ctx, config, entityId, input);
|
|
914
|
-
}));
|
|
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);
|
|
915
944
|
}
|
|
916
945
|
function createCrudBatchTransactionInput(inputs) {
|
|
917
946
|
return {
|
|
@@ -980,28 +1009,25 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
980
1009
|
filters: baseFilters
|
|
981
1010
|
};
|
|
982
1011
|
const provider = resolveCrudExtensions(ctx, config);
|
|
983
|
-
const
|
|
1012
|
+
const matchRequests = [];
|
|
984
1013
|
if (extensionFilters.length > 0) {
|
|
985
1014
|
if (!provider?.matchEntityIds) throw new TRPCError({
|
|
986
1015
|
code: "PRECONDITION_FAILED",
|
|
987
1016
|
message: "CRUD extension filtering is not available"
|
|
988
1017
|
});
|
|
989
|
-
|
|
1018
|
+
matchRequests.push(provider.matchEntityIds({
|
|
990
1019
|
id: target.id,
|
|
991
1020
|
filters: extensionFilters,
|
|
992
1021
|
joinOperator: input.joinOperator,
|
|
993
1022
|
limit: 5e3
|
|
994
|
-
});
|
|
995
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1023
|
+
}));
|
|
996
1024
|
}
|
|
997
|
-
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
matchedSets.push(new Set(matchedIds$1));
|
|
1004
|
-
} 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({
|
|
1005
1031
|
table,
|
|
1006
1032
|
search,
|
|
1007
1033
|
searchColumns
|
|
@@ -1009,6 +1035,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1009
1035
|
code: "PRECONDITION_FAILED",
|
|
1010
1036
|
message: "CRUD search is not available"
|
|
1011
1037
|
});
|
|
1038
|
+
const matchedSets = (await Promise.all(matchRequests)).map((matchedIds$1) => new Set(matchedIds$1));
|
|
1012
1039
|
if (matchedSets.length === 0) return {
|
|
1013
1040
|
...input,
|
|
1014
1041
|
filters: baseFilters
|
|
@@ -1029,11 +1056,17 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
1029
1056
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
1030
1057
|
const term = typeof search === "string" ? search.trim() : "";
|
|
1031
1058
|
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
1032
|
-
const conditions = searchColumns.
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
+
});
|
|
1037
1070
|
if (conditions.length === 0) return void 0;
|
|
1038
1071
|
if (conditions.length === 1) return conditions[0];
|
|
1039
1072
|
return or(...conditions);
|
|
@@ -1129,6 +1162,34 @@ function createCrudRouter(config) {
|
|
|
1129
1162
|
const getIdColumn = () => table[idField];
|
|
1130
1163
|
const tableColumnNames = getTableColumnNames(table);
|
|
1131
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
|
+
};
|
|
1132
1193
|
const buildWhere = (ctx, operation, additionalCondition) => {
|
|
1133
1194
|
const conditions = [];
|
|
1134
1195
|
const scopeCondition = resolved.getScope(ctx, table, operation);
|
|
@@ -1202,24 +1263,15 @@ function createCrudRouter(config) {
|
|
|
1202
1263
|
})));
|
|
1203
1264
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1204
1265
|
if (where) query = query.where(where);
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
desc: true
|
|
1208
|
-
} : void 0);
|
|
1209
|
-
if (sortField) {
|
|
1210
|
-
if (validateColumn(sortField.id, sortableColumns)) {
|
|
1211
|
-
const column = resolveColumnTarget({
|
|
1212
|
-
table,
|
|
1213
|
-
columnId: sortField.id,
|
|
1214
|
-
allowedColumns: sortableColumns
|
|
1215
|
-
});
|
|
1216
|
-
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
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);
|
|
1220
1268
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
1221
1269
|
if (where) countQuery = countQuery.where(where);
|
|
1222
|
-
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;
|
|
1223
1275
|
return {
|
|
1224
1276
|
data: enrichedData,
|
|
1225
1277
|
total: count,
|
|
@@ -1401,7 +1453,10 @@ function createCrudRouter(config) {
|
|
|
1401
1453
|
let updated = input.ids.map((id) => ({ [idField]: id }));
|
|
1402
1454
|
if (Object.keys(data).length > 0) updated = await writeCtx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
|
|
1403
1455
|
else updated = await writeCtx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
|
|
1404
|
-
await
|
|
1456
|
+
await persistCrudExtensionValueWrites(writeCtx, config, updated.flatMap((row) => {
|
|
1457
|
+
const extensionRow = createCrudExtensionValueWrite(row[idField], splitUpdate);
|
|
1458
|
+
return extensionRow ? [extensionRow] : [];
|
|
1459
|
+
}), true);
|
|
1405
1460
|
return { updated: updated.length };
|
|
1406
1461
|
});
|
|
1407
1462
|
};
|
|
@@ -1497,21 +1552,15 @@ function createCrudRouter(config) {
|
|
|
1497
1552
|
})));
|
|
1498
1553
|
let query = ctx.db.select().from(table).$dynamic();
|
|
1499
1554
|
if (where) query = query.where(where);
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
1503
|
-
const column = resolveColumnTarget({
|
|
1504
|
-
table,
|
|
1505
|
-
columnId: sortField.id,
|
|
1506
|
-
allowedColumns: sortableColumns
|
|
1507
|
-
});
|
|
1508
|
-
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
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);
|
|
1512
1557
|
let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
|
|
1513
1558
|
if (where) countQuery = countQuery.where(where);
|
|
1514
|
-
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;
|
|
1515
1564
|
return {
|
|
1516
1565
|
data: enrichedData,
|
|
1517
1566
|
total,
|
|
@@ -1531,6 +1580,7 @@ function createCrudRouter(config) {
|
|
|
1531
1580
|
const doCreateMany = async (items) => {
|
|
1532
1581
|
const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
|
|
1533
1582
|
assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
|
|
1583
|
+
assertCrudExtraValuesMappableForRows(splitItems, idField);
|
|
1534
1584
|
return runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(splitItems), async (writeCtx) => {
|
|
1535
1585
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1536
1586
|
const values = splitItems.map(({ data }) => ({
|
|
@@ -1584,6 +1634,7 @@ function createCrudRouter(config) {
|
|
|
1584
1634
|
failed
|
|
1585
1635
|
};
|
|
1586
1636
|
assertCrudExtraValuesWritableForRows(ctx, config, validRows);
|
|
1637
|
+
assertCrudExtraValuesMappableForRows(validRows, idField);
|
|
1587
1638
|
const { insertedCount, updatedCount } = await runCrudExtensionWriteTransaction(ctx, config, createCrudBatchTransactionInput(validRows), async (writeCtx) => {
|
|
1588
1639
|
const injectData = resolved.getInject(writeCtx, "create");
|
|
1589
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
|
-
"@internal/tsdown-config": "0.1.0",
|
|
53
51
|
"@internal/tsconfig": "0.1.0",
|
|
52
|
+
"@internal/eslint-config": "0.3.0",
|
|
53
|
+
"@internal/vitest-config": "0.1.0",
|
|
54
54
|
"@internal/prettier-config": "0.0.1",
|
|
55
|
-
"@internal/
|
|
55
|
+
"@internal/tsdown-config": "0.1.0"
|
|
56
56
|
},
|
|
57
57
|
"prettier": "@internal/prettier-config",
|
|
58
58
|
"scripts": {
|