@wordrhyme/auto-crud-server 1.0.5 → 1.0.7
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 +54 -0
- package/dist/index.cjs +95 -34
- package/dist/index.d.cts +53 -13
- package/dist/index.d.ts +46 -6
- package/dist/index.js +95 -34
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -489,6 +489,8 @@ interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
|
|
|
489
489
|
|
|
490
490
|
// ========== 其他配置 ==========
|
|
491
491
|
idField?: string; // ID 字段名,默认 "id"
|
|
492
|
+
filterableColumns?: CrudColumnRef<TTable>[]; // 可过滤字段白名单
|
|
493
|
+
sortableColumns?: CrudColumnRef<TTable>[]; // 可排序字段白名单
|
|
492
494
|
omitFields?: string[]; // 自动派生时排除的字段
|
|
493
495
|
// 默认 ["id", "createdAt", "updatedAt"]
|
|
494
496
|
}
|
|
@@ -1030,6 +1032,55 @@ export const tasksRouter = createCrudRouter({
|
|
|
1030
1032
|
});
|
|
1031
1033
|
```
|
|
1032
1034
|
|
|
1035
|
+
普通字段不需要特殊配置,前端仍然传字段 id:
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
filters: [{ id: 'title', value: 'bug', operator: 'iLike', variant: 'text', filterId: 'title' }],
|
|
1039
|
+
sort: [{ id: 'createdAt', desc: true }],
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
jsonb/i18n 字段可以在对应字段配置里声明读取哪个 JSON field。服务端会显式生成
|
|
1043
|
+
text expression,不会自动把所有 jsonb 字段都 cast 成 text:
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
export const productsRouter = createCrudRouter({
|
|
1047
|
+
table: shopProducts,
|
|
1048
|
+
idField: 'spuId',
|
|
1049
|
+
filterableColumns: [
|
|
1050
|
+
{ id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
1051
|
+
'status',
|
|
1052
|
+
'categoryId',
|
|
1053
|
+
],
|
|
1054
|
+
sortableColumns: [
|
|
1055
|
+
{ id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
1056
|
+
'createdAt',
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
上面的 `name` 过滤会生成类似 SQL:
|
|
1062
|
+
|
|
1063
|
+
```sql
|
|
1064
|
+
case jsonb_typeof(name)
|
|
1065
|
+
when 'object' then coalesce(name ->> 'zh-CN', name ->> 'en-US', name ->> 'en', name ->> 'zh', '')
|
|
1066
|
+
when 'string' then name #>> '{}'
|
|
1067
|
+
else ''
|
|
1068
|
+
end ilike '%关键词%'
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
复杂业务仍可用 `expression` 显式接管查询目标:
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
import { sql } from 'drizzle-orm';
|
|
1075
|
+
|
|
1076
|
+
createCrudRouter({
|
|
1077
|
+
table: products,
|
|
1078
|
+
filterableColumns: [
|
|
1079
|
+
{ id: 'displayName', expression: ({ table }) => sql<string>`lower(${table.name})` },
|
|
1080
|
+
],
|
|
1081
|
+
});
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1033
1084
|
---
|
|
1034
1085
|
|
|
1035
1086
|
## 📦 导出的类型
|
|
@@ -1043,6 +1094,9 @@ export {
|
|
|
1043
1094
|
createCrudRouter,
|
|
1044
1095
|
} from './routers/_factory';
|
|
1045
1096
|
export type {
|
|
1097
|
+
CrudColumnConfig,
|
|
1098
|
+
CrudColumnExpression,
|
|
1099
|
+
CrudColumnRef,
|
|
1046
1100
|
CrudRouterConfig,
|
|
1047
1101
|
CrudMiddleware,
|
|
1048
1102
|
GetInput,
|
package/dist/index.cjs
CHANGED
|
@@ -44,8 +44,15 @@ const publicProcedure = t.procedure;
|
|
|
44
44
|
/**
|
|
45
45
|
* 检查列是否为空
|
|
46
46
|
*/
|
|
47
|
+
function toSqlTarget(column) {
|
|
48
|
+
return drizzle_orm.sql`${column}`;
|
|
49
|
+
}
|
|
50
|
+
function isBooleanColumn(column) {
|
|
51
|
+
return "dataType" in column && column.dataType === "boolean";
|
|
52
|
+
}
|
|
47
53
|
function isEmpty(column) {
|
|
48
|
-
|
|
54
|
+
const expr = toSqlTarget(column);
|
|
55
|
+
return (0, drizzle_orm.or)((0, drizzle_orm.isNull)(expr), (0, drizzle_orm.eq)(expr, ""));
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* 安全解析日期时间戳
|
|
@@ -68,22 +75,24 @@ function safeParseNumber(value) {
|
|
|
68
75
|
const num = Number(value);
|
|
69
76
|
return Number.isNaN(num) ? null : num;
|
|
70
77
|
}
|
|
71
|
-
function filterColumns({ table, filters, joinOperator }) {
|
|
78
|
+
function filterColumns({ table, filters, joinOperator, resolveColumn }) {
|
|
72
79
|
const joinFn = joinOperator === "and" ? drizzle_orm.and : drizzle_orm.or;
|
|
73
80
|
const validConditions = filters.map((filter) => {
|
|
74
|
-
const column = getColumn(table, filter.id);
|
|
81
|
+
const column = resolveColumn ? resolveColumn(filter.id) : getColumn(table, filter.id);
|
|
82
|
+
if (!column) return void 0;
|
|
83
|
+
const expr = toSqlTarget(column);
|
|
75
84
|
switch (filter.operator) {
|
|
76
|
-
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.ilike)(
|
|
77
|
-
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.notIlike)(
|
|
85
|
+
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.ilike)(expr, `%${filter.value}%`) : void 0;
|
|
86
|
+
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.notIlike)(expr, `%${filter.value}%`) : void 0;
|
|
78
87
|
case "eq":
|
|
79
|
-
if (column
|
|
88
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return (0, drizzle_orm.eq)(expr, filter.value === "true");
|
|
80
89
|
if (filter.variant === "dateRange" && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
81
90
|
const startDate = safeParseDate(filter.value[0]);
|
|
82
91
|
const endDate = safeParseDate(filter.value[1]);
|
|
83
92
|
if (!startDate && !endDate) return void 0;
|
|
84
93
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
85
94
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
86
|
-
return (0, drizzle_orm.and)(startDate ? (0, drizzle_orm.gte)(
|
|
95
|
+
return (0, drizzle_orm.and)(startDate ? (0, drizzle_orm.gte)(expr, startDate) : void 0, endDate ? (0, drizzle_orm.lte)(expr, endDate) : void 0);
|
|
87
96
|
}
|
|
88
97
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
89
98
|
const date = safeParseDate(filter.value);
|
|
@@ -91,72 +100,72 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
91
100
|
date.setHours(0, 0, 0, 0);
|
|
92
101
|
const end = new Date(date);
|
|
93
102
|
end.setHours(23, 59, 59, 999);
|
|
94
|
-
return (0, drizzle_orm.and)((0, drizzle_orm.gte)(
|
|
103
|
+
return (0, drizzle_orm.and)((0, drizzle_orm.gte)(expr, date), (0, drizzle_orm.lte)(expr, end));
|
|
95
104
|
}
|
|
96
|
-
return (0, drizzle_orm.eq)(
|
|
105
|
+
return (0, drizzle_orm.eq)(expr, filter.value);
|
|
97
106
|
case "ne":
|
|
98
|
-
if (column
|
|
107
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return (0, drizzle_orm.ne)(expr, filter.value === "true");
|
|
99
108
|
if (filter.variant === "date" || filter.variant === "dateRange") {
|
|
100
109
|
const date = safeParseDate(filter.value);
|
|
101
110
|
if (!date) return void 0;
|
|
102
111
|
date.setHours(0, 0, 0, 0);
|
|
103
112
|
const end = new Date(date);
|
|
104
113
|
end.setHours(23, 59, 59, 999);
|
|
105
|
-
return (0, drizzle_orm.or)((0, drizzle_orm.lt)(
|
|
114
|
+
return (0, drizzle_orm.or)((0, drizzle_orm.lt)(expr, date), (0, drizzle_orm.gt)(expr, end));
|
|
106
115
|
}
|
|
107
|
-
return (0, drizzle_orm.ne)(
|
|
116
|
+
return (0, drizzle_orm.ne)(expr, filter.value);
|
|
108
117
|
case "inArray":
|
|
109
|
-
if (Array.isArray(filter.value)) return (0, drizzle_orm.inArray)(
|
|
118
|
+
if (Array.isArray(filter.value)) return (0, drizzle_orm.inArray)(expr, filter.value);
|
|
110
119
|
return;
|
|
111
120
|
case "notInArray":
|
|
112
|
-
if (Array.isArray(filter.value)) return (0, drizzle_orm.notInArray)(
|
|
121
|
+
if (Array.isArray(filter.value)) return (0, drizzle_orm.notInArray)(expr, filter.value);
|
|
113
122
|
return;
|
|
114
123
|
case "lt":
|
|
115
124
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
116
125
|
const num = safeParseNumber(filter.value);
|
|
117
|
-
return num !== null ? (0, drizzle_orm.lt)(
|
|
126
|
+
return num !== null ? (0, drizzle_orm.lt)(expr, num) : void 0;
|
|
118
127
|
}
|
|
119
128
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
120
129
|
const date = safeParseDate(filter.value);
|
|
121
130
|
if (!date) return void 0;
|
|
122
131
|
date.setHours(23, 59, 59, 999);
|
|
123
|
-
return (0, drizzle_orm.lt)(
|
|
132
|
+
return (0, drizzle_orm.lt)(expr, date);
|
|
124
133
|
}
|
|
125
134
|
return;
|
|
126
135
|
case "lte":
|
|
127
136
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
128
137
|
const num = safeParseNumber(filter.value);
|
|
129
|
-
return num !== null ? (0, drizzle_orm.lte)(
|
|
138
|
+
return num !== null ? (0, drizzle_orm.lte)(expr, num) : void 0;
|
|
130
139
|
}
|
|
131
140
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
132
141
|
const date = safeParseDate(filter.value);
|
|
133
142
|
if (!date) return void 0;
|
|
134
143
|
date.setHours(23, 59, 59, 999);
|
|
135
|
-
return (0, drizzle_orm.lte)(
|
|
144
|
+
return (0, drizzle_orm.lte)(expr, date);
|
|
136
145
|
}
|
|
137
146
|
return;
|
|
138
147
|
case "gt":
|
|
139
148
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
140
149
|
const num = safeParseNumber(filter.value);
|
|
141
|
-
return num !== null ? (0, drizzle_orm.gt)(
|
|
150
|
+
return num !== null ? (0, drizzle_orm.gt)(expr, num) : void 0;
|
|
142
151
|
}
|
|
143
152
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
144
153
|
const date = safeParseDate(filter.value);
|
|
145
154
|
if (!date) return void 0;
|
|
146
155
|
date.setHours(0, 0, 0, 0);
|
|
147
|
-
return (0, drizzle_orm.gt)(
|
|
156
|
+
return (0, drizzle_orm.gt)(expr, date);
|
|
148
157
|
}
|
|
149
158
|
return;
|
|
150
159
|
case "gte":
|
|
151
160
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
152
161
|
const num = safeParseNumber(filter.value);
|
|
153
|
-
return num !== null ? (0, drizzle_orm.gte)(
|
|
162
|
+
return num !== null ? (0, drizzle_orm.gte)(expr, num) : void 0;
|
|
154
163
|
}
|
|
155
164
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
156
165
|
const date = safeParseDate(filter.value);
|
|
157
166
|
if (!date) return void 0;
|
|
158
167
|
date.setHours(0, 0, 0, 0);
|
|
159
|
-
return (0, drizzle_orm.gte)(
|
|
168
|
+
return (0, drizzle_orm.gte)(expr, date);
|
|
160
169
|
}
|
|
161
170
|
return;
|
|
162
171
|
case "isBetween":
|
|
@@ -166,15 +175,15 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
166
175
|
if (!startDate && !endDate) return void 0;
|
|
167
176
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
168
177
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
169
|
-
return (0, drizzle_orm.and)(startDate ? (0, drizzle_orm.gte)(
|
|
178
|
+
return (0, drizzle_orm.and)(startDate ? (0, drizzle_orm.gte)(expr, startDate) : void 0, endDate ? (0, drizzle_orm.lte)(expr, endDate) : void 0);
|
|
170
179
|
}
|
|
171
180
|
if ((filter.variant === "number" || filter.variant === "range") && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
172
181
|
const firstValue = safeParseNumber(filter.value[0]);
|
|
173
182
|
const secondValue = safeParseNumber(filter.value[1]);
|
|
174
183
|
if (firstValue === null && secondValue === null) return;
|
|
175
|
-
if (firstValue !== null && secondValue === null) return (0, drizzle_orm.eq)(
|
|
176
|
-
if (firstValue === null && secondValue !== null) return (0, drizzle_orm.eq)(
|
|
177
|
-
return (0, drizzle_orm.and)(firstValue !== null ? (0, drizzle_orm.gte)(
|
|
184
|
+
if (firstValue !== null && secondValue === null) return (0, drizzle_orm.eq)(expr, firstValue);
|
|
185
|
+
if (firstValue === null && secondValue !== null) return (0, drizzle_orm.eq)(expr, secondValue);
|
|
186
|
+
return (0, drizzle_orm.and)(firstValue !== null ? (0, drizzle_orm.gte)(expr, firstValue) : void 0, secondValue !== null ? (0, drizzle_orm.lte)(expr, secondValue) : void 0);
|
|
178
187
|
}
|
|
179
188
|
return;
|
|
180
189
|
case "isRelativeToToday":
|
|
@@ -199,11 +208,11 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
199
208
|
break;
|
|
200
209
|
default: return;
|
|
201
210
|
}
|
|
202
|
-
return (0, drizzle_orm.and)((0, drizzle_orm.gte)(
|
|
211
|
+
return (0, drizzle_orm.and)((0, drizzle_orm.gte)(expr, startDate), (0, drizzle_orm.lte)(expr, endDate));
|
|
203
212
|
}
|
|
204
213
|
return;
|
|
205
|
-
case "isEmpty": return isEmpty(
|
|
206
|
-
case "isNotEmpty": return (0, drizzle_orm.not)(isEmpty(
|
|
214
|
+
case "isEmpty": return isEmpty(expr);
|
|
215
|
+
case "isNotEmpty": return (0, drizzle_orm.not)(isEmpty(expr));
|
|
207
216
|
default: throw new Error(`Unsupported operator: ${filter.operator}`);
|
|
208
217
|
}
|
|
209
218
|
}).filter((condition) => condition !== void 0);
|
|
@@ -562,9 +571,43 @@ function resolveSchemas(config) {
|
|
|
562
571
|
updateSchema
|
|
563
572
|
};
|
|
564
573
|
}
|
|
574
|
+
function getColumnRefId(columnRef) {
|
|
575
|
+
return typeof columnRef === "string" ? columnRef : columnRef.id;
|
|
576
|
+
}
|
|
577
|
+
function findColumnRef(columnId, allowedColumns) {
|
|
578
|
+
return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
|
|
579
|
+
}
|
|
565
580
|
function validateColumn(columnId, allowedColumns) {
|
|
566
581
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
567
|
-
return
|
|
582
|
+
return findColumnRef(columnId, allowedColumns) !== void 0;
|
|
583
|
+
}
|
|
584
|
+
function getTableColumn(table, columnId) {
|
|
585
|
+
return table[columnId];
|
|
586
|
+
}
|
|
587
|
+
function normalizeJsonFields(jsonField) {
|
|
588
|
+
return (Array.isArray(jsonField) ? jsonField : [jsonField]).filter((field) => field.length > 0);
|
|
589
|
+
}
|
|
590
|
+
function buildJsonFieldTextExpression(column, jsonField) {
|
|
591
|
+
const fields = normalizeJsonFields(jsonField);
|
|
592
|
+
if (fields.length === 0) throw new Error("[createCrudRouter] jsonField must include at least one json key.");
|
|
593
|
+
const objectFields = fields.map((field) => drizzle_orm.sql`${column} ->> ${field}`);
|
|
594
|
+
return drizzle_orm.sql`case jsonb_typeof(${column})
|
|
595
|
+
when 'object' then coalesce(${drizzle_orm.sql.join(objectFields, drizzle_orm.sql`, `)}, '')
|
|
596
|
+
when 'string' then ${column} #>> '{}'
|
|
597
|
+
else ''
|
|
598
|
+
end`;
|
|
599
|
+
}
|
|
600
|
+
function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
601
|
+
const columnRef = findColumnRef(columnId, allowedColumns);
|
|
602
|
+
if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
|
|
603
|
+
if (columnRef && typeof columnRef !== "string") {
|
|
604
|
+
if (columnRef.expression) return columnRef.expression({ table });
|
|
605
|
+
const column = getTableColumn(table, columnRef.id);
|
|
606
|
+
if (!column) return void 0;
|
|
607
|
+
if (columnRef.jsonField) return buildJsonFieldTextExpression(column, columnRef.jsonField);
|
|
608
|
+
return column;
|
|
609
|
+
}
|
|
610
|
+
return getTableColumn(table, columnId);
|
|
568
611
|
}
|
|
569
612
|
/**
|
|
570
613
|
* 创建 CRUD Router
|
|
@@ -667,14 +710,23 @@ function createCrudRouter(config) {
|
|
|
667
710
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
668
711
|
table,
|
|
669
712
|
filters: validatedFilters,
|
|
670
|
-
joinOperator: listInput$1.joinOperator
|
|
713
|
+
joinOperator: listInput$1.joinOperator,
|
|
714
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
715
|
+
table,
|
|
716
|
+
columnId: String(columnId),
|
|
717
|
+
allowedColumns: filterableColumns
|
|
718
|
+
})
|
|
671
719
|
}) : void 0);
|
|
672
720
|
let query = ctx.db.select().from(table).$dynamic();
|
|
673
721
|
if (where) query = query.where(where);
|
|
674
722
|
if (listInput$1.sort?.length) {
|
|
675
723
|
const sortField = listInput$1.sort[0];
|
|
676
724
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
677
|
-
const column =
|
|
725
|
+
const column = resolveColumnTarget({
|
|
726
|
+
table,
|
|
727
|
+
columnId: sortField.id,
|
|
728
|
+
allowedColumns: sortableColumns
|
|
729
|
+
});
|
|
678
730
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
679
731
|
}
|
|
680
732
|
}
|
|
@@ -868,14 +920,23 @@ function createCrudRouter(config) {
|
|
|
868
920
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
869
921
|
table,
|
|
870
922
|
filters: validatedFilters,
|
|
871
|
-
joinOperator: exportInput$1.joinOperator ?? "and"
|
|
923
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
924
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
925
|
+
table,
|
|
926
|
+
columnId: String(columnId),
|
|
927
|
+
allowedColumns: filterableColumns
|
|
928
|
+
})
|
|
872
929
|
}) : void 0);
|
|
873
930
|
let query = ctx.db.select().from(table).$dynamic();
|
|
874
931
|
if (where) query = query.where(where);
|
|
875
932
|
if (exportInput$1.sort?.length) {
|
|
876
933
|
const sortField = exportInput$1.sort[0];
|
|
877
934
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
878
|
-
const column =
|
|
935
|
+
const column = resolveColumnTarget({
|
|
936
|
+
table,
|
|
937
|
+
columnId: sortField.id,
|
|
938
|
+
allowedColumns: sortableColumns
|
|
939
|
+
});
|
|
879
940
|
if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
|
|
880
941
|
}
|
|
881
942
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import * as
|
|
3
|
+
import * as _trpc_server0 from "@trpc/server";
|
|
4
4
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
5
|
-
import { SQL } from "drizzle-orm";
|
|
5
|
+
import { AnyColumn, SQL } from "drizzle-orm";
|
|
6
6
|
|
|
7
7
|
//#region src/trpc.d.ts
|
|
8
8
|
/**
|
|
@@ -12,13 +12,13 @@ import { SQL } from "drizzle-orm";
|
|
|
12
12
|
interface Context {
|
|
13
13
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
14
14
|
}
|
|
15
|
-
declare const router:
|
|
15
|
+
declare const router: _trpc_server0.TRPCRouterBuilder<{
|
|
16
16
|
ctx: Context;
|
|
17
17
|
meta: object;
|
|
18
|
-
errorShape:
|
|
18
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
19
19
|
transformer: true;
|
|
20
20
|
}>;
|
|
21
|
-
declare const publicProcedure:
|
|
21
|
+
declare const publicProcedure: _trpc_server0.TRPCProcedureBuilder<Context, object, object, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, false>;
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/types/config.d.ts
|
|
24
24
|
type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
|
|
@@ -29,6 +29,31 @@ type WriteOperation = 'create' | 'update';
|
|
|
29
29
|
*/
|
|
30
30
|
type TableColumnKeys<T extends PgTable> = keyof T['_']['columns'];
|
|
31
31
|
type AnyProcedure = any;
|
|
32
|
+
/**
|
|
33
|
+
* 过滤/排序字段的自定义 SQL expression。
|
|
34
|
+
*
|
|
35
|
+
* 普通字段无需配置 expression,直接使用字段名即可;jsonb、i18n、派生字段等
|
|
36
|
+
* 需要自定义查询目标时,可通过 expression 显式返回 Drizzle SQL 片段或列对象。
|
|
37
|
+
*/
|
|
38
|
+
type CrudColumnExpression<TTable extends PgTable = PgTable> = (params: {
|
|
39
|
+
table: TTable;
|
|
40
|
+
}) => SQL | AnyColumn;
|
|
41
|
+
/**
|
|
42
|
+
* 过滤/排序字段配置。
|
|
43
|
+
*
|
|
44
|
+
* - string: 普通表字段,继续使用 table[id]
|
|
45
|
+
* - object.jsonField: 从 jsonb 字段中提取一个或多个 key 作为文本查询目标
|
|
46
|
+
* - object.expression: 高级场景自定义 SQL expression
|
|
47
|
+
*/
|
|
48
|
+
interface CrudColumnConfig<TTable extends PgTable = PgTable> {
|
|
49
|
+
/** 前端传入的逻辑字段名,同时默认对应 table 上的列名 */
|
|
50
|
+
id: string;
|
|
51
|
+
/** jsonb 对象内部要读取的字段;数组按顺序 coalesce 作为 fallback */
|
|
52
|
+
jsonField?: string | string[];
|
|
53
|
+
/** 高级场景自定义查询目标,优先级高于 jsonField */
|
|
54
|
+
expression?: CrudColumnExpression<TTable>;
|
|
55
|
+
}
|
|
56
|
+
type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
|
|
32
57
|
interface SoftDeleteConfig {
|
|
33
58
|
/** 软删除标记列名 */
|
|
34
59
|
column: string;
|
|
@@ -496,10 +521,25 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
496
521
|
omitFields?: TableColumnKeys<TTable>[];
|
|
497
522
|
/** ID 字段名,默认 "id" */
|
|
498
523
|
idField?: string;
|
|
499
|
-
/**
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
524
|
+
/**
|
|
525
|
+
* 可过滤的列白名单。
|
|
526
|
+
*
|
|
527
|
+
* 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
|
|
528
|
+
* 由服务端显式生成 text expression,前端仍然传 id。
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* filterableColumns: [
|
|
532
|
+
* 'status',
|
|
533
|
+
* { id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
534
|
+
* ]
|
|
535
|
+
*/
|
|
536
|
+
filterableColumns?: CrudColumnRef<TTable>[];
|
|
537
|
+
/**
|
|
538
|
+
* 可排序的列白名单。
|
|
539
|
+
*
|
|
540
|
+
* 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
|
|
541
|
+
*/
|
|
542
|
+
sortableColumns?: CrudColumnRef<TTable>[];
|
|
503
543
|
/** 批量操作的最大数量限制,默认 100 */
|
|
504
544
|
maxBatchSize?: number;
|
|
505
545
|
/**
|
|
@@ -771,12 +811,12 @@ type CrudHookEventMap<PluginId extends string, ResourceName extends string, TCre
|
|
|
771
811
|
} } & { [K in `${PluginId}.${ResourceName}.createMany`]: TCreate[] };
|
|
772
812
|
//#endregion
|
|
773
813
|
//#region src/routers/index.d.ts
|
|
774
|
-
declare const appRouter:
|
|
814
|
+
declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
|
|
775
815
|
ctx: Context;
|
|
776
816
|
meta: object;
|
|
777
|
-
errorShape:
|
|
817
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
778
818
|
transformer: true;
|
|
779
|
-
},
|
|
819
|
+
}, _trpc_server0.TRPCDecorateCreateRouterOptions<{}>>;
|
|
780
820
|
type AppRouter = typeof appRouter;
|
|
781
821
|
//#endregion
|
|
782
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
|
822
|
+
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { SQL } from "drizzle-orm";
|
|
2
|
+
import { AnyColumn, SQL } from "drizzle-orm";
|
|
3
3
|
import * as _trpc_server2 from "@trpc/server";
|
|
4
4
|
import superjson from "superjson";
|
|
5
5
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
@@ -30,6 +30,31 @@ type WriteOperation = 'create' | 'update';
|
|
|
30
30
|
*/
|
|
31
31
|
type TableColumnKeys<T extends PgTable> = keyof T['_']['columns'];
|
|
32
32
|
type AnyProcedure = any;
|
|
33
|
+
/**
|
|
34
|
+
* 过滤/排序字段的自定义 SQL expression。
|
|
35
|
+
*
|
|
36
|
+
* 普通字段无需配置 expression,直接使用字段名即可;jsonb、i18n、派生字段等
|
|
37
|
+
* 需要自定义查询目标时,可通过 expression 显式返回 Drizzle SQL 片段或列对象。
|
|
38
|
+
*/
|
|
39
|
+
type CrudColumnExpression<TTable extends PgTable = PgTable> = (params: {
|
|
40
|
+
table: TTable;
|
|
41
|
+
}) => SQL | AnyColumn;
|
|
42
|
+
/**
|
|
43
|
+
* 过滤/排序字段配置。
|
|
44
|
+
*
|
|
45
|
+
* - string: 普通表字段,继续使用 table[id]
|
|
46
|
+
* - object.jsonField: 从 jsonb 字段中提取一个或多个 key 作为文本查询目标
|
|
47
|
+
* - object.expression: 高级场景自定义 SQL expression
|
|
48
|
+
*/
|
|
49
|
+
interface CrudColumnConfig<TTable extends PgTable = PgTable> {
|
|
50
|
+
/** 前端传入的逻辑字段名,同时默认对应 table 上的列名 */
|
|
51
|
+
id: string;
|
|
52
|
+
/** jsonb 对象内部要读取的字段;数组按顺序 coalesce 作为 fallback */
|
|
53
|
+
jsonField?: string | string[];
|
|
54
|
+
/** 高级场景自定义查询目标,优先级高于 jsonField */
|
|
55
|
+
expression?: CrudColumnExpression<TTable>;
|
|
56
|
+
}
|
|
57
|
+
type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
|
|
33
58
|
interface SoftDeleteConfig {
|
|
34
59
|
/** 软删除标记列名 */
|
|
35
60
|
column: string;
|
|
@@ -497,10 +522,25 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
497
522
|
omitFields?: TableColumnKeys<TTable>[];
|
|
498
523
|
/** ID 字段名,默认 "id" */
|
|
499
524
|
idField?: string;
|
|
500
|
-
/**
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
525
|
+
/**
|
|
526
|
+
* 可过滤的列白名单。
|
|
527
|
+
*
|
|
528
|
+
* 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
|
|
529
|
+
* 由服务端显式生成 text expression,前端仍然传 id。
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* filterableColumns: [
|
|
533
|
+
* 'status',
|
|
534
|
+
* { id: 'name', jsonField: ['zh-CN', 'en-US', 'en', 'zh'] },
|
|
535
|
+
* ]
|
|
536
|
+
*/
|
|
537
|
+
filterableColumns?: CrudColumnRef<TTable>[];
|
|
538
|
+
/**
|
|
539
|
+
* 可排序的列白名单。
|
|
540
|
+
*
|
|
541
|
+
* 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
|
|
542
|
+
*/
|
|
543
|
+
sortableColumns?: CrudColumnRef<TTable>[];
|
|
504
544
|
/** 批量操作的最大数量限制,默认 100 */
|
|
505
545
|
maxBatchSize?: number;
|
|
506
546
|
/**
|
|
@@ -780,4 +820,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
780
820
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
781
821
|
type AppRouter = typeof appRouter;
|
|
782
822
|
//#endregion
|
|
783
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
|
823
|
+
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,15 @@ const publicProcedure = t.procedure;
|
|
|
15
15
|
/**
|
|
16
16
|
* 检查列是否为空
|
|
17
17
|
*/
|
|
18
|
+
function toSqlTarget(column) {
|
|
19
|
+
return sql`${column}`;
|
|
20
|
+
}
|
|
21
|
+
function isBooleanColumn(column) {
|
|
22
|
+
return "dataType" in column && column.dataType === "boolean";
|
|
23
|
+
}
|
|
18
24
|
function isEmpty(column) {
|
|
19
|
-
|
|
25
|
+
const expr = toSqlTarget(column);
|
|
26
|
+
return or(isNull(expr), eq(expr, ""));
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* 安全解析日期时间戳
|
|
@@ -39,22 +46,24 @@ function safeParseNumber(value) {
|
|
|
39
46
|
const num = Number(value);
|
|
40
47
|
return Number.isNaN(num) ? null : num;
|
|
41
48
|
}
|
|
42
|
-
function filterColumns({ table, filters, joinOperator }) {
|
|
49
|
+
function filterColumns({ table, filters, joinOperator, resolveColumn }) {
|
|
43
50
|
const joinFn = joinOperator === "and" ? and : or;
|
|
44
51
|
const validConditions = filters.map((filter) => {
|
|
45
|
-
const column = getColumn(table, filter.id);
|
|
52
|
+
const column = resolveColumn ? resolveColumn(filter.id) : getColumn(table, filter.id);
|
|
53
|
+
if (!column) return void 0;
|
|
54
|
+
const expr = toSqlTarget(column);
|
|
46
55
|
switch (filter.operator) {
|
|
47
|
-
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? ilike(
|
|
48
|
-
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(
|
|
56
|
+
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? ilike(expr, `%${filter.value}%`) : void 0;
|
|
57
|
+
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(expr, `%${filter.value}%`) : void 0;
|
|
49
58
|
case "eq":
|
|
50
|
-
if (column
|
|
59
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return eq(expr, filter.value === "true");
|
|
51
60
|
if (filter.variant === "dateRange" && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
52
61
|
const startDate = safeParseDate(filter.value[0]);
|
|
53
62
|
const endDate = safeParseDate(filter.value[1]);
|
|
54
63
|
if (!startDate && !endDate) return void 0;
|
|
55
64
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
56
65
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
57
|
-
return and(startDate ? gte(
|
|
66
|
+
return and(startDate ? gte(expr, startDate) : void 0, endDate ? lte(expr, endDate) : void 0);
|
|
58
67
|
}
|
|
59
68
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
60
69
|
const date = safeParseDate(filter.value);
|
|
@@ -62,72 +71,72 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
62
71
|
date.setHours(0, 0, 0, 0);
|
|
63
72
|
const end = new Date(date);
|
|
64
73
|
end.setHours(23, 59, 59, 999);
|
|
65
|
-
return and(gte(
|
|
74
|
+
return and(gte(expr, date), lte(expr, end));
|
|
66
75
|
}
|
|
67
|
-
return eq(
|
|
76
|
+
return eq(expr, filter.value);
|
|
68
77
|
case "ne":
|
|
69
|
-
if (column
|
|
78
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return ne(expr, filter.value === "true");
|
|
70
79
|
if (filter.variant === "date" || filter.variant === "dateRange") {
|
|
71
80
|
const date = safeParseDate(filter.value);
|
|
72
81
|
if (!date) return void 0;
|
|
73
82
|
date.setHours(0, 0, 0, 0);
|
|
74
83
|
const end = new Date(date);
|
|
75
84
|
end.setHours(23, 59, 59, 999);
|
|
76
|
-
return or(lt(
|
|
85
|
+
return or(lt(expr, date), gt(expr, end));
|
|
77
86
|
}
|
|
78
|
-
return ne(
|
|
87
|
+
return ne(expr, filter.value);
|
|
79
88
|
case "inArray":
|
|
80
|
-
if (Array.isArray(filter.value)) return inArray(
|
|
89
|
+
if (Array.isArray(filter.value)) return inArray(expr, filter.value);
|
|
81
90
|
return;
|
|
82
91
|
case "notInArray":
|
|
83
|
-
if (Array.isArray(filter.value)) return notInArray(
|
|
92
|
+
if (Array.isArray(filter.value)) return notInArray(expr, filter.value);
|
|
84
93
|
return;
|
|
85
94
|
case "lt":
|
|
86
95
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
87
96
|
const num = safeParseNumber(filter.value);
|
|
88
|
-
return num !== null ? lt(
|
|
97
|
+
return num !== null ? lt(expr, num) : void 0;
|
|
89
98
|
}
|
|
90
99
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
91
100
|
const date = safeParseDate(filter.value);
|
|
92
101
|
if (!date) return void 0;
|
|
93
102
|
date.setHours(23, 59, 59, 999);
|
|
94
|
-
return lt(
|
|
103
|
+
return lt(expr, date);
|
|
95
104
|
}
|
|
96
105
|
return;
|
|
97
106
|
case "lte":
|
|
98
107
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
99
108
|
const num = safeParseNumber(filter.value);
|
|
100
|
-
return num !== null ? lte(
|
|
109
|
+
return num !== null ? lte(expr, num) : void 0;
|
|
101
110
|
}
|
|
102
111
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
103
112
|
const date = safeParseDate(filter.value);
|
|
104
113
|
if (!date) return void 0;
|
|
105
114
|
date.setHours(23, 59, 59, 999);
|
|
106
|
-
return lte(
|
|
115
|
+
return lte(expr, date);
|
|
107
116
|
}
|
|
108
117
|
return;
|
|
109
118
|
case "gt":
|
|
110
119
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
111
120
|
const num = safeParseNumber(filter.value);
|
|
112
|
-
return num !== null ? gt(
|
|
121
|
+
return num !== null ? gt(expr, num) : void 0;
|
|
113
122
|
}
|
|
114
123
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
115
124
|
const date = safeParseDate(filter.value);
|
|
116
125
|
if (!date) return void 0;
|
|
117
126
|
date.setHours(0, 0, 0, 0);
|
|
118
|
-
return gt(
|
|
127
|
+
return gt(expr, date);
|
|
119
128
|
}
|
|
120
129
|
return;
|
|
121
130
|
case "gte":
|
|
122
131
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
123
132
|
const num = safeParseNumber(filter.value);
|
|
124
|
-
return num !== null ? gte(
|
|
133
|
+
return num !== null ? gte(expr, num) : void 0;
|
|
125
134
|
}
|
|
126
135
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
127
136
|
const date = safeParseDate(filter.value);
|
|
128
137
|
if (!date) return void 0;
|
|
129
138
|
date.setHours(0, 0, 0, 0);
|
|
130
|
-
return gte(
|
|
139
|
+
return gte(expr, date);
|
|
131
140
|
}
|
|
132
141
|
return;
|
|
133
142
|
case "isBetween":
|
|
@@ -137,15 +146,15 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
137
146
|
if (!startDate && !endDate) return void 0;
|
|
138
147
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
139
148
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
140
|
-
return and(startDate ? gte(
|
|
149
|
+
return and(startDate ? gte(expr, startDate) : void 0, endDate ? lte(expr, endDate) : void 0);
|
|
141
150
|
}
|
|
142
151
|
if ((filter.variant === "number" || filter.variant === "range") && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
143
152
|
const firstValue = safeParseNumber(filter.value[0]);
|
|
144
153
|
const secondValue = safeParseNumber(filter.value[1]);
|
|
145
154
|
if (firstValue === null && secondValue === null) return;
|
|
146
|
-
if (firstValue !== null && secondValue === null) return eq(
|
|
147
|
-
if (firstValue === null && secondValue !== null) return eq(
|
|
148
|
-
return and(firstValue !== null ? gte(
|
|
155
|
+
if (firstValue !== null && secondValue === null) return eq(expr, firstValue);
|
|
156
|
+
if (firstValue === null && secondValue !== null) return eq(expr, secondValue);
|
|
157
|
+
return and(firstValue !== null ? gte(expr, firstValue) : void 0, secondValue !== null ? lte(expr, secondValue) : void 0);
|
|
149
158
|
}
|
|
150
159
|
return;
|
|
151
160
|
case "isRelativeToToday":
|
|
@@ -170,11 +179,11 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
170
179
|
break;
|
|
171
180
|
default: return;
|
|
172
181
|
}
|
|
173
|
-
return and(gte(
|
|
182
|
+
return and(gte(expr, startDate), lte(expr, endDate));
|
|
174
183
|
}
|
|
175
184
|
return;
|
|
176
|
-
case "isEmpty": return isEmpty(
|
|
177
|
-
case "isNotEmpty": return not(isEmpty(
|
|
185
|
+
case "isEmpty": return isEmpty(expr);
|
|
186
|
+
case "isNotEmpty": return not(isEmpty(expr));
|
|
178
187
|
default: throw new Error(`Unsupported operator: ${filter.operator}`);
|
|
179
188
|
}
|
|
180
189
|
}).filter((condition) => condition !== void 0);
|
|
@@ -533,9 +542,43 @@ function resolveSchemas(config) {
|
|
|
533
542
|
updateSchema
|
|
534
543
|
};
|
|
535
544
|
}
|
|
545
|
+
function getColumnRefId(columnRef) {
|
|
546
|
+
return typeof columnRef === "string" ? columnRef : columnRef.id;
|
|
547
|
+
}
|
|
548
|
+
function findColumnRef(columnId, allowedColumns) {
|
|
549
|
+
return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
|
|
550
|
+
}
|
|
536
551
|
function validateColumn(columnId, allowedColumns) {
|
|
537
552
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
538
|
-
return
|
|
553
|
+
return findColumnRef(columnId, allowedColumns) !== void 0;
|
|
554
|
+
}
|
|
555
|
+
function getTableColumn(table, columnId) {
|
|
556
|
+
return table[columnId];
|
|
557
|
+
}
|
|
558
|
+
function normalizeJsonFields(jsonField) {
|
|
559
|
+
return (Array.isArray(jsonField) ? jsonField : [jsonField]).filter((field) => field.length > 0);
|
|
560
|
+
}
|
|
561
|
+
function buildJsonFieldTextExpression(column, jsonField) {
|
|
562
|
+
const fields = normalizeJsonFields(jsonField);
|
|
563
|
+
if (fields.length === 0) throw new Error("[createCrudRouter] jsonField must include at least one json key.");
|
|
564
|
+
const objectFields = fields.map((field) => sql`${column} ->> ${field}`);
|
|
565
|
+
return sql`case jsonb_typeof(${column})
|
|
566
|
+
when 'object' then coalesce(${sql.join(objectFields, sql`, `)}, '')
|
|
567
|
+
when 'string' then ${column} #>> '{}'
|
|
568
|
+
else ''
|
|
569
|
+
end`;
|
|
570
|
+
}
|
|
571
|
+
function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
572
|
+
const columnRef = findColumnRef(columnId, allowedColumns);
|
|
573
|
+
if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
|
|
574
|
+
if (columnRef && typeof columnRef !== "string") {
|
|
575
|
+
if (columnRef.expression) return columnRef.expression({ table });
|
|
576
|
+
const column = getTableColumn(table, columnRef.id);
|
|
577
|
+
if (!column) return void 0;
|
|
578
|
+
if (columnRef.jsonField) return buildJsonFieldTextExpression(column, columnRef.jsonField);
|
|
579
|
+
return column;
|
|
580
|
+
}
|
|
581
|
+
return getTableColumn(table, columnId);
|
|
539
582
|
}
|
|
540
583
|
/**
|
|
541
584
|
* 创建 CRUD Router
|
|
@@ -638,14 +681,23 @@ function createCrudRouter(config) {
|
|
|
638
681
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
639
682
|
table,
|
|
640
683
|
filters: validatedFilters,
|
|
641
|
-
joinOperator: listInput$1.joinOperator
|
|
684
|
+
joinOperator: listInput$1.joinOperator,
|
|
685
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
686
|
+
table,
|
|
687
|
+
columnId: String(columnId),
|
|
688
|
+
allowedColumns: filterableColumns
|
|
689
|
+
})
|
|
642
690
|
}) : void 0);
|
|
643
691
|
let query = ctx.db.select().from(table).$dynamic();
|
|
644
692
|
if (where) query = query.where(where);
|
|
645
693
|
if (listInput$1.sort?.length) {
|
|
646
694
|
const sortField = listInput$1.sort[0];
|
|
647
695
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
648
|
-
const column =
|
|
696
|
+
const column = resolveColumnTarget({
|
|
697
|
+
table,
|
|
698
|
+
columnId: sortField.id,
|
|
699
|
+
allowedColumns: sortableColumns
|
|
700
|
+
});
|
|
649
701
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
650
702
|
}
|
|
651
703
|
}
|
|
@@ -839,14 +891,23 @@ function createCrudRouter(config) {
|
|
|
839
891
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
840
892
|
table,
|
|
841
893
|
filters: validatedFilters,
|
|
842
|
-
joinOperator: exportInput$1.joinOperator ?? "and"
|
|
894
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
895
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
896
|
+
table,
|
|
897
|
+
columnId: String(columnId),
|
|
898
|
+
allowedColumns: filterableColumns
|
|
899
|
+
})
|
|
843
900
|
}) : void 0);
|
|
844
901
|
let query = ctx.db.select().from(table).$dynamic();
|
|
845
902
|
if (where) query = query.where(where);
|
|
846
903
|
if (exportInput$1.sort?.length) {
|
|
847
904
|
const sortField = exportInput$1.sort[0];
|
|
848
905
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
849
|
-
const column =
|
|
906
|
+
const column = resolveColumnTarget({
|
|
907
|
+
table,
|
|
908
|
+
columnId: sortField.id,
|
|
909
|
+
allowedColumns: sortableColumns
|
|
910
|
+
});
|
|
850
911
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
851
912
|
}
|
|
852
913
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.7",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"vitest": "^4.0.18",
|
|
49
49
|
"zod": "^4.1.13",
|
|
50
50
|
"@internal/eslint-config": "0.3.0",
|
|
51
|
-
"@internal/tsdown-config": "0.1.0",
|
|
52
|
-
"@internal/vitest-config": "0.1.0",
|
|
53
51
|
"@internal/tsconfig": "0.1.0",
|
|
54
|
-
"@internal/prettier-config": "0.0.1"
|
|
52
|
+
"@internal/prettier-config": "0.0.1",
|
|
53
|
+
"@internal/tsdown-config": "0.1.0",
|
|
54
|
+
"@internal/vitest-config": "0.1.0"
|
|
55
55
|
},
|
|
56
56
|
"prettier": "@internal/prettier-config",
|
|
57
57
|
"scripts": {
|