@wordrhyme/auto-crud-server 1.1.8 → 1.3.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 +34 -0
- package/dist/index.cjs +185 -17
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +185 -17
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -939,6 +939,40 @@ export const db = drizzle(client, { schema });
|
|
|
939
939
|
export const createContext = () => ({ db });
|
|
940
940
|
```
|
|
941
941
|
|
|
942
|
+
### 使用业务时区解析日期过滤
|
|
943
|
+
|
|
944
|
+
AutoCrud 默认保留历史行为:使用服务器本地时区解析日期过滤。如果宿主已经有用户或租户的业务时区,可在 tRPC context 中提供 `resolveDateRange`,将日历日期转换为 UTC 半开区间:
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
import type { DateRangeResolver } from '@wordrhyme/auto-crud-server';
|
|
948
|
+
|
|
949
|
+
const resolveDateRange: DateRangeResolver = (filter) => {
|
|
950
|
+
// 由宿主根据 filter.value、filter.operator 和当前业务时区进行转换。
|
|
951
|
+
// resolveCalendarFilterToUtc 应返回 UTC Date 对象。
|
|
952
|
+
return resolveCalendarFilterToUtc(filter, getBusinessTimeZone());
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
export const createContext = () => ({
|
|
956
|
+
db,
|
|
957
|
+
resolveDateRange,
|
|
958
|
+
});
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
Resolver 返回值遵循以下契约:
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
interface DateFilterRange {
|
|
965
|
+
start?: Date; // 日历范围的 UTC 起点,包含
|
|
966
|
+
endExclusive?: Date; // UTC 终点,不包含
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
- 返回 `undefined`:使用原有服务器本地时区逻辑。
|
|
971
|
+
- 返回非空范围:该结果是权威结果,不再回退到本地时区。
|
|
972
|
+
- `lt` / `gte` 需要 `start`;`lte` / `gt` 需要 `endExclusive`。
|
|
973
|
+
- `eq`、`ne`、`isBetween`、`isRelativeToToday` 可使用单边或双边范围。
|
|
974
|
+
- 无效日期、空范围、反向范围或缺少操作符所需边界会抛出 `RangeError`,避免过滤条件意外扩大。
|
|
975
|
+
|
|
942
976
|
### 与 Next.js 集成
|
|
943
977
|
|
|
944
978
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -75,12 +75,73 @@ function safeParseNumber(value) {
|
|
|
75
75
|
const num = Number(value);
|
|
76
76
|
return Number.isNaN(num) ? null : num;
|
|
77
77
|
}
|
|
78
|
-
function
|
|
78
|
+
function isValidDate(value) {
|
|
79
|
+
return value instanceof Date && !Number.isNaN(value.getTime());
|
|
80
|
+
}
|
|
81
|
+
function isDateRangeOperator(operator) {
|
|
82
|
+
switch (operator) {
|
|
83
|
+
case "eq":
|
|
84
|
+
case "ne":
|
|
85
|
+
case "lt":
|
|
86
|
+
case "lte":
|
|
87
|
+
case "gt":
|
|
88
|
+
case "gte":
|
|
89
|
+
case "isBetween":
|
|
90
|
+
case "isRelativeToToday": return true;
|
|
91
|
+
case "iLike":
|
|
92
|
+
case "notILike":
|
|
93
|
+
case "inArray":
|
|
94
|
+
case "notInArray":
|
|
95
|
+
case "isEmpty":
|
|
96
|
+
case "isNotEmpty": return false;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function normalizeDateRange(range) {
|
|
101
|
+
if (range.start !== void 0 && !isValidDate(range.start)) throw new RangeError("resolveDateRange returned an invalid start boundary");
|
|
102
|
+
if (range.endExclusive !== void 0 && !isValidDate(range.endExclusive)) throw new RangeError("resolveDateRange returned an invalid endExclusive boundary");
|
|
103
|
+
const { start, endExclusive } = range;
|
|
104
|
+
if (!start && !endExclusive) throw new RangeError("resolveDateRange must return at least one date boundary");
|
|
105
|
+
if (start && endExclusive && start >= endExclusive) throw new RangeError("resolveDateRange requires start to be before endExclusive");
|
|
106
|
+
return {
|
|
107
|
+
start,
|
|
108
|
+
endExclusive
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function requireDateBoundary(boundary, name, operator) {
|
|
112
|
+
if (!boundary) throw new RangeError(`resolveDateRange must return ${name} for operator ${operator}`);
|
|
113
|
+
return boundary;
|
|
114
|
+
}
|
|
115
|
+
function resolvedDateCondition(expr, operator, range) {
|
|
116
|
+
const { start, endExclusive } = range;
|
|
117
|
+
switch (operator) {
|
|
118
|
+
case "eq":
|
|
119
|
+
case "isBetween":
|
|
120
|
+
case "isRelativeToToday":
|
|
121
|
+
if (start && endExclusive) return (0, drizzle_orm.and)((0, drizzle_orm.gte)(expr, start), (0, drizzle_orm.lt)(expr, endExclusive));
|
|
122
|
+
if (start) return (0, drizzle_orm.gte)(expr, start);
|
|
123
|
+
return (0, drizzle_orm.lt)(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
124
|
+
case "ne":
|
|
125
|
+
if (start && endExclusive) return (0, drizzle_orm.or)((0, drizzle_orm.lt)(expr, start), (0, drizzle_orm.gte)(expr, endExclusive));
|
|
126
|
+
if (start) return (0, drizzle_orm.lt)(expr, start);
|
|
127
|
+
return (0, drizzle_orm.gte)(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
128
|
+
case "lt": return (0, drizzle_orm.lt)(expr, requireDateBoundary(start, "start", operator));
|
|
129
|
+
case "lte": return (0, drizzle_orm.lt)(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
130
|
+
case "gt": return (0, drizzle_orm.gte)(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
131
|
+
case "gte": return (0, drizzle_orm.gte)(expr, requireDateBoundary(start, "start", operator));
|
|
132
|
+
}
|
|
133
|
+
throw new Error("Unsupported resolved date operator");
|
|
134
|
+
}
|
|
135
|
+
function filterColumns({ table, filters, joinOperator, resolveColumn, resolveDateRange }) {
|
|
79
136
|
const joinFn = joinOperator === "and" ? drizzle_orm.and : drizzle_orm.or;
|
|
80
137
|
const validConditions = filters.map((filter) => {
|
|
81
138
|
const column = resolveColumn ? resolveColumn(filter.id) : getColumn(table, filter.id);
|
|
82
139
|
if (!column) return void 0;
|
|
83
140
|
const expr = toSqlTarget(column);
|
|
141
|
+
if ((filter.variant === "date" || filter.variant === "dateRange") && isDateRangeOperator(filter.operator)) {
|
|
142
|
+
const resolvedRange = resolveDateRange?.(filter);
|
|
143
|
+
if (resolvedRange !== void 0) return resolvedDateCondition(expr, filter.operator, normalizeDateRange(resolvedRange));
|
|
144
|
+
}
|
|
84
145
|
switch (filter.operator) {
|
|
85
146
|
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.ilike)(expr, `%${filter.value}%`) : void 0;
|
|
86
147
|
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? (0, drizzle_orm.notIlike)(expr, `%${filter.value}%`) : void 0;
|
|
@@ -621,12 +682,87 @@ function getColumnRefId(columnRef) {
|
|
|
621
682
|
return typeof columnRef === "string" ? columnRef : columnRef.id;
|
|
622
683
|
}
|
|
623
684
|
function findColumnRef(columnId, allowedColumns) {
|
|
685
|
+
if (!Array.isArray(allowedColumns)) return void 0;
|
|
624
686
|
return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
|
|
625
687
|
}
|
|
626
688
|
function validateColumn(columnId, allowedColumns) {
|
|
627
|
-
if (
|
|
689
|
+
if (allowedColumns === false) return false;
|
|
690
|
+
if (allowedColumns === void 0) return true;
|
|
628
691
|
return findColumnRef(columnId, allowedColumns) !== void 0;
|
|
629
692
|
}
|
|
693
|
+
function resolveColumnCapability(config, nextKey, legacyKey) {
|
|
694
|
+
const nextValue = config[nextKey];
|
|
695
|
+
const legacyValue = config[legacyKey];
|
|
696
|
+
if (nextValue !== void 0 && legacyValue !== void 0) throw new Error(`[createCrudRouter] "${nextKey}" and "${legacyKey}" cannot be used together.`);
|
|
697
|
+
const value = nextValue !== void 0 ? nextValue : legacyValue;
|
|
698
|
+
if (value === void 0) return void 0;
|
|
699
|
+
if (value === false) return false;
|
|
700
|
+
if (Array.isArray(value)) return value;
|
|
701
|
+
throw new Error(`[createCrudRouter] "${nextKey}" must be an array of column refs or false.`);
|
|
702
|
+
}
|
|
703
|
+
function readCapabilityFields(columns) {
|
|
704
|
+
if (columns === false) return [];
|
|
705
|
+
if (Array.isArray(columns)) return columns.map(getColumnRefId);
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
function buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns) {
|
|
709
|
+
const searchFields = readCapabilityFields(searchColumns) ?? [];
|
|
710
|
+
return {
|
|
711
|
+
search: {
|
|
712
|
+
enabled: searchFields.length > 0,
|
|
713
|
+
fields: searchFields
|
|
714
|
+
},
|
|
715
|
+
filters: {
|
|
716
|
+
enabled: filterableColumns !== false,
|
|
717
|
+
fields: readCapabilityFields(filterableColumns)
|
|
718
|
+
},
|
|
719
|
+
sort: {
|
|
720
|
+
enabled: sortableColumns !== false,
|
|
721
|
+
fields: readCapabilityFields(sortableColumns)
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function uniqueCapabilityFields(...fields) {
|
|
726
|
+
return Array.from(new Set(fields.flatMap((fieldList) => fieldList ?? [])));
|
|
727
|
+
}
|
|
728
|
+
function mergeSearchCapability(base, extension, disabled) {
|
|
729
|
+
if (disabled) return {
|
|
730
|
+
enabled: false,
|
|
731
|
+
fields: []
|
|
732
|
+
};
|
|
733
|
+
const fields = uniqueCapabilityFields(base.fields, extension?.fields);
|
|
734
|
+
return {
|
|
735
|
+
enabled: base.enabled || extension?.enabled === true || fields.length > 0,
|
|
736
|
+
fields
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function mergeFieldCapability(base, extension, disabled) {
|
|
740
|
+
if (disabled) return {
|
|
741
|
+
enabled: false,
|
|
742
|
+
fields: []
|
|
743
|
+
};
|
|
744
|
+
if (!(base.enabled || extension?.enabled === true)) return {
|
|
745
|
+
enabled: false,
|
|
746
|
+
fields: []
|
|
747
|
+
};
|
|
748
|
+
return {
|
|
749
|
+
enabled: true,
|
|
750
|
+
fields: base.fields === null || extension?.fields === null ? null : uniqueCapabilityFields(base.fields, extension?.fields)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function mergeCrudCapabilities(base, extension, disabled) {
|
|
754
|
+
return {
|
|
755
|
+
search: mergeSearchCapability(base.search, extension?.search, disabled.search),
|
|
756
|
+
filters: mergeFieldCapability(base.filters, extension?.filters, disabled.filters),
|
|
757
|
+
sort: mergeFieldCapability(base.sort, extension?.sort, disabled.sort)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function withCrudCapabilities(metadata, capabilities, disabled) {
|
|
761
|
+
return {
|
|
762
|
+
...metadata ?? {},
|
|
763
|
+
capabilities: mergeCrudCapabilities(capabilities, metadata?.capabilities, disabled)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
630
766
|
function getTableColumn(table, columnId) {
|
|
631
767
|
return table[columnId];
|
|
632
768
|
}
|
|
@@ -645,7 +781,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
|
|
|
645
781
|
}
|
|
646
782
|
function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
647
783
|
const columnRef = findColumnRef(columnId, allowedColumns);
|
|
648
|
-
if (
|
|
784
|
+
if (allowedColumns === false) return;
|
|
785
|
+
if (Array.isArray(allowedColumns) && columnRef === void 0) return;
|
|
649
786
|
if (columnRef && typeof columnRef !== "string") {
|
|
650
787
|
if (columnRef.expression) return columnRef.expression({ table });
|
|
651
788
|
const column = getTableColumn(table, columnRef.id);
|
|
@@ -841,7 +978,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
|
|
|
841
978
|
});
|
|
842
979
|
}
|
|
843
980
|
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
844
|
-
if (allowedColumns
|
|
981
|
+
if (allowedColumns === false) return false;
|
|
982
|
+
if (Array.isArray(allowedColumns)) return resolveColumnTarget({
|
|
845
983
|
table,
|
|
846
984
|
columnId,
|
|
847
985
|
allowedColumns
|
|
@@ -856,11 +994,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
856
994
|
const filters = input.filters ?? [];
|
|
857
995
|
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
858
996
|
if (!target || filters.length === 0 && !search) return input;
|
|
997
|
+
const filtersDisabled = filterableColumns === false;
|
|
998
|
+
const searchDisabled = searchColumns === false;
|
|
859
999
|
const baseFilters = [];
|
|
860
1000
|
const extensionFilters = [];
|
|
861
|
-
for (const filter of filters)
|
|
862
|
-
|
|
863
|
-
|
|
1001
|
+
for (const filter of filters) {
|
|
1002
|
+
if (filtersDisabled) continue;
|
|
1003
|
+
if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
1004
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
1005
|
+
else extensionFilters.push(filter);
|
|
1006
|
+
}
|
|
864
1007
|
if (extensionFilters.length === 0 && !search) return {
|
|
865
1008
|
...input,
|
|
866
1009
|
filters: baseFilters
|
|
@@ -880,14 +1023,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
880
1023
|
});
|
|
881
1024
|
matchedSets.push(new Set(matchedIds$1));
|
|
882
1025
|
}
|
|
883
|
-
if (search && provider?.searchEntityIds) {
|
|
1026
|
+
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
884
1027
|
const matchedIds$1 = await provider.searchEntityIds({
|
|
885
1028
|
id: target.id,
|
|
886
1029
|
search,
|
|
887
1030
|
limit: 5e3
|
|
888
1031
|
});
|
|
889
1032
|
matchedSets.push(new Set(matchedIds$1));
|
|
890
|
-
} else if (search && !buildCrudSearchCondition({
|
|
1033
|
+
} else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
891
1034
|
table,
|
|
892
1035
|
search,
|
|
893
1036
|
searchColumns
|
|
@@ -914,7 +1057,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
914
1057
|
}
|
|
915
1058
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
916
1059
|
const term = typeof search === "string" ? search.trim() : "";
|
|
917
|
-
if (!term || !searchColumns || searchColumns.length === 0) return
|
|
1060
|
+
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
918
1061
|
const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
|
|
919
1062
|
table,
|
|
920
1063
|
columnId: getColumnRefId(columnRef),
|
|
@@ -943,7 +1086,16 @@ function combineConditions(...conditions) {
|
|
|
943
1086
|
* @typeParam TUpdate - 更新输入类型
|
|
944
1087
|
*/
|
|
945
1088
|
function createCrudRouter(config) {
|
|
946
|
-
const { table, idField = "id",
|
|
1089
|
+
const { table, idField = "id", maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
1090
|
+
const searchColumns = resolveColumnCapability(config, "search", "searchColumns");
|
|
1091
|
+
const filterableColumns = resolveColumnCapability(config, "filters", "filterableColumns");
|
|
1092
|
+
const sortableColumns = resolveColumnCapability(config, "sort", "sortableColumns");
|
|
1093
|
+
const capabilities = buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns);
|
|
1094
|
+
const disabledCapabilities = {
|
|
1095
|
+
search: searchColumns === false,
|
|
1096
|
+
filters: filterableColumns === false,
|
|
1097
|
+
sort: sortableColumns === false
|
|
1098
|
+
};
|
|
947
1099
|
const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
|
|
948
1100
|
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
949
1101
|
const listOutputSchema = zod.z.object({
|
|
@@ -956,7 +1108,21 @@ function createCrudRouter(config) {
|
|
|
956
1108
|
const metadataOutputSchema = zod.z.object({
|
|
957
1109
|
schema: zod.z.unknown().optional(),
|
|
958
1110
|
fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
959
|
-
errors: zod.z.array(zod.z.string()).optional()
|
|
1111
|
+
errors: zod.z.array(zod.z.string()).optional(),
|
|
1112
|
+
capabilities: zod.z.object({
|
|
1113
|
+
search: zod.z.object({
|
|
1114
|
+
enabled: zod.z.boolean(),
|
|
1115
|
+
fields: zod.z.array(zod.z.string())
|
|
1116
|
+
}),
|
|
1117
|
+
filters: zod.z.object({
|
|
1118
|
+
enabled: zod.z.boolean(),
|
|
1119
|
+
fields: zod.z.array(zod.z.string()).nullable()
|
|
1120
|
+
}),
|
|
1121
|
+
sort: zod.z.object({
|
|
1122
|
+
enabled: zod.z.boolean(),
|
|
1123
|
+
fields: zod.z.array(zod.z.string()).nullable()
|
|
1124
|
+
})
|
|
1125
|
+
})
|
|
960
1126
|
});
|
|
961
1127
|
const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
|
|
962
1128
|
const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
|
|
@@ -1033,10 +1199,10 @@ function createCrudRouter(config) {
|
|
|
1033
1199
|
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
1034
1200
|
await withGuard(ctx, "list");
|
|
1035
1201
|
const target = readCrudTarget(config);
|
|
1036
|
-
if (!target) return {};
|
|
1202
|
+
if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
|
|
1037
1203
|
const provider = resolveCrudExtensions(ctx, config);
|
|
1038
|
-
if (!provider?.getMetadata) return {};
|
|
1039
|
-
return provider.getMetadata({ id: target.id });
|
|
1204
|
+
if (!provider?.getMetadata) return withCrudCapabilities({}, capabilities, disabledCapabilities);
|
|
1205
|
+
return withCrudCapabilities(await provider.getMetadata({ id: target.id }), capabilities, disabledCapabilities);
|
|
1040
1206
|
}),
|
|
1041
1207
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
1042
1208
|
await withGuard(ctx, "list");
|
|
@@ -1056,7 +1222,8 @@ function createCrudRouter(config) {
|
|
|
1056
1222
|
columnId: id,
|
|
1057
1223
|
allowedColumns: filterableColumns
|
|
1058
1224
|
});
|
|
1059
|
-
}
|
|
1225
|
+
},
|
|
1226
|
+
resolveDateRange: ctx.resolveDateRange
|
|
1060
1227
|
}) : void 0, buildCrudSearchCondition({
|
|
1061
1228
|
table,
|
|
1062
1229
|
search: effectiveInput.search,
|
|
@@ -1350,7 +1517,8 @@ function createCrudRouter(config) {
|
|
|
1350
1517
|
columnId: id,
|
|
1351
1518
|
allowedColumns: filterableColumns
|
|
1352
1519
|
});
|
|
1353
|
-
}
|
|
1520
|
+
},
|
|
1521
|
+
resolveDateRange: ctx.resolveDateRange
|
|
1354
1522
|
}) : void 0, buildCrudSearchCondition({
|
|
1355
1523
|
table,
|
|
1356
1524
|
search: effectiveInput.search,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
+
import { AnyColumn, SQL, Table } from "drizzle-orm";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
4
|
import * as _trpc_server2 from "@trpc/server";
|
|
4
5
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
5
|
-
import { AnyColumn, SQL } from "drizzle-orm";
|
|
6
6
|
|
|
7
|
+
//#region src/lib/filter-columns.d.ts
|
|
8
|
+
/** 过滤器变体类型 */
|
|
9
|
+
type FilterVariant = 'text' | 'number' | 'range' | 'date' | 'dateRange' | 'boolean' | 'select' | 'multiSelect';
|
|
10
|
+
/** 过滤器操作符 */
|
|
11
|
+
type FilterOperator = 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'iLike' | 'notILike' | 'inArray' | 'notInArray' | 'isBetween' | 'isRelativeToToday' | 'isEmpty' | 'isNotEmpty';
|
|
12
|
+
/** 扩展的列过滤器 */
|
|
13
|
+
interface ExtendedColumnFilter<T extends Table = Table> {
|
|
14
|
+
id: keyof T | string;
|
|
15
|
+
value: unknown;
|
|
16
|
+
variant: FilterVariant;
|
|
17
|
+
operator: FilterOperator;
|
|
18
|
+
}
|
|
19
|
+
/** Host-resolved UTC boundaries for a calendar-date filter. */
|
|
20
|
+
interface DateFilterRange {
|
|
21
|
+
start?: Date;
|
|
22
|
+
endExclusive?: Date;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Optional host adapter for date filters.
|
|
26
|
+
*
|
|
27
|
+
* AutoCrud intentionally does not choose a time zone. A host that has business
|
|
28
|
+
* time-zone context can resolve the filter to UTC boundaries. Returning
|
|
29
|
+
* undefined preserves the legacy server-local parsing behavior. A non-empty
|
|
30
|
+
* result is authoritative and must contain valid boundaries for the operator.
|
|
31
|
+
*/
|
|
32
|
+
type DateRangeResolver<T extends Table = Table> = (filter: ExtendedColumnFilter<T>) => DateFilterRange | undefined;
|
|
33
|
+
//#endregion
|
|
7
34
|
//#region src/trpc.d.ts
|
|
8
35
|
/**
|
|
9
36
|
* tRPC Context 类型
|
|
@@ -11,6 +38,7 @@ import { AnyColumn, SQL } from "drizzle-orm";
|
|
|
11
38
|
*/
|
|
12
39
|
interface Context {
|
|
13
40
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
41
|
+
resolveDateRange?: DateRangeResolver;
|
|
14
42
|
}
|
|
15
43
|
declare const router: _trpc_server2.TRPCRouterBuilder<{
|
|
16
44
|
ctx: Context;
|
|
@@ -54,6 +82,21 @@ interface CrudColumnConfig<TTable extends PgTable = PgTable> {
|
|
|
54
82
|
expression?: CrudColumnExpression<TTable>;
|
|
55
83
|
}
|
|
56
84
|
type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
|
|
85
|
+
type CrudColumnCapability<TTable extends PgTable = PgTable> = false | CrudColumnRef<TTable>[];
|
|
86
|
+
interface CrudCapabilityMetadata {
|
|
87
|
+
search: {
|
|
88
|
+
enabled: boolean;
|
|
89
|
+
fields: string[];
|
|
90
|
+
};
|
|
91
|
+
filters: {
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
fields: string[] | null;
|
|
94
|
+
};
|
|
95
|
+
sort: {
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
fields: string[] | null;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
57
100
|
interface SoftDeleteConfig {
|
|
58
101
|
/** 软删除标记列名 */
|
|
59
102
|
column: string;
|
|
@@ -133,6 +176,8 @@ interface CrudExtensionMetadata {
|
|
|
133
176
|
schema?: unknown;
|
|
134
177
|
fields?: Record<string, unknown>;
|
|
135
178
|
errors?: string[];
|
|
179
|
+
/** 扩展提供的查询能力,会与 router 基础能力合并。 */
|
|
180
|
+
capabilities?: CrudCapabilityMetadata;
|
|
136
181
|
}
|
|
137
182
|
interface CrudExtensionFilter {
|
|
138
183
|
id: string;
|
|
@@ -586,9 +631,34 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
586
631
|
omitFields?: TableColumnKeys<TTable>[];
|
|
587
632
|
/** ID 字段名,默认 "id" */
|
|
588
633
|
idField?: string;
|
|
634
|
+
/**
|
|
635
|
+
* 全局搜索列白名单。
|
|
636
|
+
*
|
|
637
|
+
* 不配置时不启用全局搜索;传数组时启用并只搜索指定字段;传 false 显式禁用。
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* search: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
|
|
641
|
+
*/
|
|
642
|
+
search?: CrudColumnCapability<TTable>;
|
|
643
|
+
/**
|
|
644
|
+
* 可过滤列白名单。
|
|
645
|
+
*
|
|
646
|
+
* 不配置时保持兼容行为:所有真实表字段可过滤;传数组时只允许指定字段;
|
|
647
|
+
* 传 false 显式禁用过滤。
|
|
648
|
+
*/
|
|
649
|
+
filters?: CrudColumnCapability<TTable>;
|
|
650
|
+
/**
|
|
651
|
+
* 可排序列白名单。
|
|
652
|
+
*
|
|
653
|
+
* 不配置时保持兼容行为:所有真实表字段可排序;传数组时只允许指定字段;
|
|
654
|
+
* 传 false 显式禁用排序。
|
|
655
|
+
*/
|
|
656
|
+
sort?: CrudColumnCapability<TTable>;
|
|
589
657
|
/**
|
|
590
658
|
* 可过滤的列白名单。
|
|
591
659
|
*
|
|
660
|
+
* @deprecated Use `filters` instead.
|
|
661
|
+
*
|
|
592
662
|
* 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
|
|
593
663
|
* 由服务端显式生成 text expression,前端仍然传 id。
|
|
594
664
|
*
|
|
@@ -602,6 +672,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
602
672
|
/**
|
|
603
673
|
* 全局搜索列白名单。
|
|
604
674
|
*
|
|
675
|
+
* @deprecated Use `search` instead.
|
|
676
|
+
*
|
|
605
677
|
* 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
|
|
606
678
|
* 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
|
|
607
679
|
* `CrudExtensionsProvider.searchEntityIds` 处理。
|
|
@@ -613,6 +685,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
613
685
|
/**
|
|
614
686
|
* 可排序的列白名单。
|
|
615
687
|
*
|
|
688
|
+
* @deprecated Use `sort` instead.
|
|
689
|
+
*
|
|
616
690
|
* 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
|
|
617
691
|
*/
|
|
618
692
|
sortableColumns?: CrudColumnRef<TTable>[];
|
|
@@ -898,4 +972,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
898
972
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
899
973
|
type AppRouter = typeof appRouter;
|
|
900
974
|
//#endregion
|
|
901
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { AnyColumn, SQL } from "drizzle-orm";
|
|
2
|
+
import { AnyColumn, SQL, Table } 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";
|
|
6
6
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
7
7
|
|
|
8
|
+
//#region src/lib/filter-columns.d.ts
|
|
9
|
+
/** 过滤器变体类型 */
|
|
10
|
+
type FilterVariant = 'text' | 'number' | 'range' | 'date' | 'dateRange' | 'boolean' | 'select' | 'multiSelect';
|
|
11
|
+
/** 过滤器操作符 */
|
|
12
|
+
type FilterOperator = 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'iLike' | 'notILike' | 'inArray' | 'notInArray' | 'isBetween' | 'isRelativeToToday' | 'isEmpty' | 'isNotEmpty';
|
|
13
|
+
/** 扩展的列过滤器 */
|
|
14
|
+
interface ExtendedColumnFilter<T extends Table = Table> {
|
|
15
|
+
id: keyof T | string;
|
|
16
|
+
value: unknown;
|
|
17
|
+
variant: FilterVariant;
|
|
18
|
+
operator: FilterOperator;
|
|
19
|
+
}
|
|
20
|
+
/** Host-resolved UTC boundaries for a calendar-date filter. */
|
|
21
|
+
interface DateFilterRange {
|
|
22
|
+
start?: Date;
|
|
23
|
+
endExclusive?: Date;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Optional host adapter for date filters.
|
|
27
|
+
*
|
|
28
|
+
* AutoCrud intentionally does not choose a time zone. A host that has business
|
|
29
|
+
* time-zone context can resolve the filter to UTC boundaries. Returning
|
|
30
|
+
* undefined preserves the legacy server-local parsing behavior. A non-empty
|
|
31
|
+
* result is authoritative and must contain valid boundaries for the operator.
|
|
32
|
+
*/
|
|
33
|
+
type DateRangeResolver<T extends Table = Table> = (filter: ExtendedColumnFilter<T>) => DateFilterRange | undefined;
|
|
34
|
+
//#endregion
|
|
8
35
|
//#region src/trpc.d.ts
|
|
9
36
|
/**
|
|
10
37
|
* tRPC Context 类型
|
|
@@ -12,6 +39,7 @@ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
|
12
39
|
*/
|
|
13
40
|
interface Context {
|
|
14
41
|
db: PostgresJsDatabase<Record<string, never>>;
|
|
42
|
+
resolveDateRange?: DateRangeResolver;
|
|
15
43
|
}
|
|
16
44
|
declare const router: _trpc_server2.TRPCRouterBuilder<{
|
|
17
45
|
ctx: Context;
|
|
@@ -55,6 +83,21 @@ interface CrudColumnConfig<TTable extends PgTable = PgTable> {
|
|
|
55
83
|
expression?: CrudColumnExpression<TTable>;
|
|
56
84
|
}
|
|
57
85
|
type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
|
|
86
|
+
type CrudColumnCapability<TTable extends PgTable = PgTable> = false | CrudColumnRef<TTable>[];
|
|
87
|
+
interface CrudCapabilityMetadata {
|
|
88
|
+
search: {
|
|
89
|
+
enabled: boolean;
|
|
90
|
+
fields: string[];
|
|
91
|
+
};
|
|
92
|
+
filters: {
|
|
93
|
+
enabled: boolean;
|
|
94
|
+
fields: string[] | null;
|
|
95
|
+
};
|
|
96
|
+
sort: {
|
|
97
|
+
enabled: boolean;
|
|
98
|
+
fields: string[] | null;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
58
101
|
interface SoftDeleteConfig {
|
|
59
102
|
/** 软删除标记列名 */
|
|
60
103
|
column: string;
|
|
@@ -134,6 +177,8 @@ interface CrudExtensionMetadata {
|
|
|
134
177
|
schema?: unknown;
|
|
135
178
|
fields?: Record<string, unknown>;
|
|
136
179
|
errors?: string[];
|
|
180
|
+
/** 扩展提供的查询能力,会与 router 基础能力合并。 */
|
|
181
|
+
capabilities?: CrudCapabilityMetadata;
|
|
137
182
|
}
|
|
138
183
|
interface CrudExtensionFilter {
|
|
139
184
|
id: string;
|
|
@@ -587,9 +632,34 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
587
632
|
omitFields?: TableColumnKeys<TTable>[];
|
|
588
633
|
/** ID 字段名,默认 "id" */
|
|
589
634
|
idField?: string;
|
|
635
|
+
/**
|
|
636
|
+
* 全局搜索列白名单。
|
|
637
|
+
*
|
|
638
|
+
* 不配置时不启用全局搜索;传数组时启用并只搜索指定字段;传 false 显式禁用。
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* search: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
|
|
642
|
+
*/
|
|
643
|
+
search?: CrudColumnCapability<TTable>;
|
|
644
|
+
/**
|
|
645
|
+
* 可过滤列白名单。
|
|
646
|
+
*
|
|
647
|
+
* 不配置时保持兼容行为:所有真实表字段可过滤;传数组时只允许指定字段;
|
|
648
|
+
* 传 false 显式禁用过滤。
|
|
649
|
+
*/
|
|
650
|
+
filters?: CrudColumnCapability<TTable>;
|
|
651
|
+
/**
|
|
652
|
+
* 可排序列白名单。
|
|
653
|
+
*
|
|
654
|
+
* 不配置时保持兼容行为:所有真实表字段可排序;传数组时只允许指定字段;
|
|
655
|
+
* 传 false 显式禁用排序。
|
|
656
|
+
*/
|
|
657
|
+
sort?: CrudColumnCapability<TTable>;
|
|
590
658
|
/**
|
|
591
659
|
* 可过滤的列白名单。
|
|
592
660
|
*
|
|
661
|
+
* @deprecated Use `filters` instead.
|
|
662
|
+
*
|
|
593
663
|
* 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
|
|
594
664
|
* 由服务端显式生成 text expression,前端仍然传 id。
|
|
595
665
|
*
|
|
@@ -603,6 +673,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
603
673
|
/**
|
|
604
674
|
* 全局搜索列白名单。
|
|
605
675
|
*
|
|
676
|
+
* @deprecated Use `search` instead.
|
|
677
|
+
*
|
|
606
678
|
* 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
|
|
607
679
|
* 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
|
|
608
680
|
* `CrudExtensionsProvider.searchEntityIds` 处理。
|
|
@@ -614,6 +686,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
|
|
|
614
686
|
/**
|
|
615
687
|
* 可排序的列白名单。
|
|
616
688
|
*
|
|
689
|
+
* @deprecated Use `sort` instead.
|
|
690
|
+
*
|
|
617
691
|
* 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
|
|
618
692
|
*/
|
|
619
693
|
sortableColumns?: CrudColumnRef<TTable>[];
|
|
@@ -899,4 +973,4 @@ declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
|
|
|
899
973
|
}, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
|
|
900
974
|
type AppRouter = typeof appRouter;
|
|
901
975
|
//#endregion
|
|
902
|
-
export { type AnyProcedure, type AppRouter, type CreateManyMiddlewareParams, type CreateMiddlewareParams, type CrudColumnConfig, type CrudColumnExpression, type CrudColumnRef, type CrudExtensionFilter, type CrudExtensionMetadata, type CrudExtensionsConfig, type CrudExtensionsProvider, type CrudHookEventMap, type CrudMiddleware, type CrudOperation, type CrudProcedures, type CrudRouterConfig, type DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -46,12 +46,73 @@ function safeParseNumber(value) {
|
|
|
46
46
|
const num = Number(value);
|
|
47
47
|
return Number.isNaN(num) ? null : num;
|
|
48
48
|
}
|
|
49
|
-
function
|
|
49
|
+
function isValidDate(value) {
|
|
50
|
+
return value instanceof Date && !Number.isNaN(value.getTime());
|
|
51
|
+
}
|
|
52
|
+
function isDateRangeOperator(operator) {
|
|
53
|
+
switch (operator) {
|
|
54
|
+
case "eq":
|
|
55
|
+
case "ne":
|
|
56
|
+
case "lt":
|
|
57
|
+
case "lte":
|
|
58
|
+
case "gt":
|
|
59
|
+
case "gte":
|
|
60
|
+
case "isBetween":
|
|
61
|
+
case "isRelativeToToday": return true;
|
|
62
|
+
case "iLike":
|
|
63
|
+
case "notILike":
|
|
64
|
+
case "inArray":
|
|
65
|
+
case "notInArray":
|
|
66
|
+
case "isEmpty":
|
|
67
|
+
case "isNotEmpty": return false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function normalizeDateRange(range) {
|
|
72
|
+
if (range.start !== void 0 && !isValidDate(range.start)) throw new RangeError("resolveDateRange returned an invalid start boundary");
|
|
73
|
+
if (range.endExclusive !== void 0 && !isValidDate(range.endExclusive)) throw new RangeError("resolveDateRange returned an invalid endExclusive boundary");
|
|
74
|
+
const { start, endExclusive } = range;
|
|
75
|
+
if (!start && !endExclusive) throw new RangeError("resolveDateRange must return at least one date boundary");
|
|
76
|
+
if (start && endExclusive && start >= endExclusive) throw new RangeError("resolveDateRange requires start to be before endExclusive");
|
|
77
|
+
return {
|
|
78
|
+
start,
|
|
79
|
+
endExclusive
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function requireDateBoundary(boundary, name, operator) {
|
|
83
|
+
if (!boundary) throw new RangeError(`resolveDateRange must return ${name} for operator ${operator}`);
|
|
84
|
+
return boundary;
|
|
85
|
+
}
|
|
86
|
+
function resolvedDateCondition(expr, operator, range) {
|
|
87
|
+
const { start, endExclusive } = range;
|
|
88
|
+
switch (operator) {
|
|
89
|
+
case "eq":
|
|
90
|
+
case "isBetween":
|
|
91
|
+
case "isRelativeToToday":
|
|
92
|
+
if (start && endExclusive) return and(gte(expr, start), lt(expr, endExclusive));
|
|
93
|
+
if (start) return gte(expr, start);
|
|
94
|
+
return lt(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
95
|
+
case "ne":
|
|
96
|
+
if (start && endExclusive) return or(lt(expr, start), gte(expr, endExclusive));
|
|
97
|
+
if (start) return lt(expr, start);
|
|
98
|
+
return gte(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
99
|
+
case "lt": return lt(expr, requireDateBoundary(start, "start", operator));
|
|
100
|
+
case "lte": return lt(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
101
|
+
case "gt": return gte(expr, requireDateBoundary(endExclusive, "endExclusive", operator));
|
|
102
|
+
case "gte": return gte(expr, requireDateBoundary(start, "start", operator));
|
|
103
|
+
}
|
|
104
|
+
throw new Error("Unsupported resolved date operator");
|
|
105
|
+
}
|
|
106
|
+
function filterColumns({ table, filters, joinOperator, resolveColumn, resolveDateRange }) {
|
|
50
107
|
const joinFn = joinOperator === "and" ? and : or;
|
|
51
108
|
const validConditions = filters.map((filter) => {
|
|
52
109
|
const column = resolveColumn ? resolveColumn(filter.id) : getColumn(table, filter.id);
|
|
53
110
|
if (!column) return void 0;
|
|
54
111
|
const expr = toSqlTarget(column);
|
|
112
|
+
if ((filter.variant === "date" || filter.variant === "dateRange") && isDateRangeOperator(filter.operator)) {
|
|
113
|
+
const resolvedRange = resolveDateRange?.(filter);
|
|
114
|
+
if (resolvedRange !== void 0) return resolvedDateCondition(expr, filter.operator, normalizeDateRange(resolvedRange));
|
|
115
|
+
}
|
|
55
116
|
switch (filter.operator) {
|
|
56
117
|
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? ilike(expr, `%${filter.value}%`) : void 0;
|
|
57
118
|
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(expr, `%${filter.value}%`) : void 0;
|
|
@@ -592,12 +653,87 @@ function getColumnRefId(columnRef) {
|
|
|
592
653
|
return typeof columnRef === "string" ? columnRef : columnRef.id;
|
|
593
654
|
}
|
|
594
655
|
function findColumnRef(columnId, allowedColumns) {
|
|
656
|
+
if (!Array.isArray(allowedColumns)) return void 0;
|
|
595
657
|
return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
|
|
596
658
|
}
|
|
597
659
|
function validateColumn(columnId, allowedColumns) {
|
|
598
|
-
if (
|
|
660
|
+
if (allowedColumns === false) return false;
|
|
661
|
+
if (allowedColumns === void 0) return true;
|
|
599
662
|
return findColumnRef(columnId, allowedColumns) !== void 0;
|
|
600
663
|
}
|
|
664
|
+
function resolveColumnCapability(config, nextKey, legacyKey) {
|
|
665
|
+
const nextValue = config[nextKey];
|
|
666
|
+
const legacyValue = config[legacyKey];
|
|
667
|
+
if (nextValue !== void 0 && legacyValue !== void 0) throw new Error(`[createCrudRouter] "${nextKey}" and "${legacyKey}" cannot be used together.`);
|
|
668
|
+
const value = nextValue !== void 0 ? nextValue : legacyValue;
|
|
669
|
+
if (value === void 0) return void 0;
|
|
670
|
+
if (value === false) return false;
|
|
671
|
+
if (Array.isArray(value)) return value;
|
|
672
|
+
throw new Error(`[createCrudRouter] "${nextKey}" must be an array of column refs or false.`);
|
|
673
|
+
}
|
|
674
|
+
function readCapabilityFields(columns) {
|
|
675
|
+
if (columns === false) return [];
|
|
676
|
+
if (Array.isArray(columns)) return columns.map(getColumnRefId);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns) {
|
|
680
|
+
const searchFields = readCapabilityFields(searchColumns) ?? [];
|
|
681
|
+
return {
|
|
682
|
+
search: {
|
|
683
|
+
enabled: searchFields.length > 0,
|
|
684
|
+
fields: searchFields
|
|
685
|
+
},
|
|
686
|
+
filters: {
|
|
687
|
+
enabled: filterableColumns !== false,
|
|
688
|
+
fields: readCapabilityFields(filterableColumns)
|
|
689
|
+
},
|
|
690
|
+
sort: {
|
|
691
|
+
enabled: sortableColumns !== false,
|
|
692
|
+
fields: readCapabilityFields(sortableColumns)
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function uniqueCapabilityFields(...fields) {
|
|
697
|
+
return Array.from(new Set(fields.flatMap((fieldList) => fieldList ?? [])));
|
|
698
|
+
}
|
|
699
|
+
function mergeSearchCapability(base, extension, disabled) {
|
|
700
|
+
if (disabled) return {
|
|
701
|
+
enabled: false,
|
|
702
|
+
fields: []
|
|
703
|
+
};
|
|
704
|
+
const fields = uniqueCapabilityFields(base.fields, extension?.fields);
|
|
705
|
+
return {
|
|
706
|
+
enabled: base.enabled || extension?.enabled === true || fields.length > 0,
|
|
707
|
+
fields
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function mergeFieldCapability(base, extension, disabled) {
|
|
711
|
+
if (disabled) return {
|
|
712
|
+
enabled: false,
|
|
713
|
+
fields: []
|
|
714
|
+
};
|
|
715
|
+
if (!(base.enabled || extension?.enabled === true)) return {
|
|
716
|
+
enabled: false,
|
|
717
|
+
fields: []
|
|
718
|
+
};
|
|
719
|
+
return {
|
|
720
|
+
enabled: true,
|
|
721
|
+
fields: base.fields === null || extension?.fields === null ? null : uniqueCapabilityFields(base.fields, extension?.fields)
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function mergeCrudCapabilities(base, extension, disabled) {
|
|
725
|
+
return {
|
|
726
|
+
search: mergeSearchCapability(base.search, extension?.search, disabled.search),
|
|
727
|
+
filters: mergeFieldCapability(base.filters, extension?.filters, disabled.filters),
|
|
728
|
+
sort: mergeFieldCapability(base.sort, extension?.sort, disabled.sort)
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function withCrudCapabilities(metadata, capabilities, disabled) {
|
|
732
|
+
return {
|
|
733
|
+
...metadata ?? {},
|
|
734
|
+
capabilities: mergeCrudCapabilities(capabilities, metadata?.capabilities, disabled)
|
|
735
|
+
};
|
|
736
|
+
}
|
|
601
737
|
function getTableColumn(table, columnId) {
|
|
602
738
|
return table[columnId];
|
|
603
739
|
}
|
|
@@ -616,7 +752,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
|
|
|
616
752
|
}
|
|
617
753
|
function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
618
754
|
const columnRef = findColumnRef(columnId, allowedColumns);
|
|
619
|
-
if (
|
|
755
|
+
if (allowedColumns === false) return;
|
|
756
|
+
if (Array.isArray(allowedColumns) && columnRef === void 0) return;
|
|
620
757
|
if (columnRef && typeof columnRef !== "string") {
|
|
621
758
|
if (columnRef.expression) return columnRef.expression({ table });
|
|
622
759
|
const column = getTableColumn(table, columnRef.id);
|
|
@@ -812,7 +949,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
|
|
|
812
949
|
});
|
|
813
950
|
}
|
|
814
951
|
function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
|
|
815
|
-
if (allowedColumns
|
|
952
|
+
if (allowedColumns === false) return false;
|
|
953
|
+
if (Array.isArray(allowedColumns)) return resolveColumnTarget({
|
|
816
954
|
table,
|
|
817
955
|
columnId,
|
|
818
956
|
allowedColumns
|
|
@@ -827,11 +965,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
827
965
|
const filters = input.filters ?? [];
|
|
828
966
|
const search = typeof input.search === "string" ? input.search.trim() : "";
|
|
829
967
|
if (!target || filters.length === 0 && !search) return input;
|
|
968
|
+
const filtersDisabled = filterableColumns === false;
|
|
969
|
+
const searchDisabled = searchColumns === false;
|
|
830
970
|
const baseFilters = [];
|
|
831
971
|
const extensionFilters = [];
|
|
832
|
-
for (const filter of filters)
|
|
833
|
-
|
|
834
|
-
|
|
972
|
+
for (const filter of filters) {
|
|
973
|
+
if (filtersDisabled) continue;
|
|
974
|
+
if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
|
|
975
|
+
else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
|
|
976
|
+
else extensionFilters.push(filter);
|
|
977
|
+
}
|
|
835
978
|
if (extensionFilters.length === 0 && !search) return {
|
|
836
979
|
...input,
|
|
837
980
|
filters: baseFilters
|
|
@@ -851,14 +994,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
851
994
|
});
|
|
852
995
|
matchedSets.push(new Set(matchedIds$1));
|
|
853
996
|
}
|
|
854
|
-
if (search && provider?.searchEntityIds) {
|
|
997
|
+
if (search && !searchDisabled && provider?.searchEntityIds) {
|
|
855
998
|
const matchedIds$1 = await provider.searchEntityIds({
|
|
856
999
|
id: target.id,
|
|
857
1000
|
search,
|
|
858
1001
|
limit: 5e3
|
|
859
1002
|
});
|
|
860
1003
|
matchedSets.push(new Set(matchedIds$1));
|
|
861
|
-
} else if (search && !buildCrudSearchCondition({
|
|
1004
|
+
} else if (search && !searchDisabled && !buildCrudSearchCondition({
|
|
862
1005
|
table,
|
|
863
1006
|
search,
|
|
864
1007
|
searchColumns
|
|
@@ -885,7 +1028,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
|
|
|
885
1028
|
}
|
|
886
1029
|
function buildCrudSearchCondition({ table, search, searchColumns }) {
|
|
887
1030
|
const term = typeof search === "string" ? search.trim() : "";
|
|
888
|
-
if (!term || !searchColumns || searchColumns.length === 0) return
|
|
1031
|
+
if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
|
|
889
1032
|
const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
|
|
890
1033
|
table,
|
|
891
1034
|
columnId: getColumnRefId(columnRef),
|
|
@@ -914,7 +1057,16 @@ function combineConditions(...conditions) {
|
|
|
914
1057
|
* @typeParam TUpdate - 更新输入类型
|
|
915
1058
|
*/
|
|
916
1059
|
function createCrudRouter(config) {
|
|
917
|
-
const { table, idField = "id",
|
|
1060
|
+
const { table, idField = "id", maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
|
|
1061
|
+
const searchColumns = resolveColumnCapability(config, "search", "searchColumns");
|
|
1062
|
+
const filterableColumns = resolveColumnCapability(config, "filters", "filterableColumns");
|
|
1063
|
+
const sortableColumns = resolveColumnCapability(config, "sort", "sortableColumns");
|
|
1064
|
+
const capabilities = buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns);
|
|
1065
|
+
const disabledCapabilities = {
|
|
1066
|
+
search: searchColumns === false,
|
|
1067
|
+
filters: filterableColumns === false,
|
|
1068
|
+
sort: sortableColumns === false
|
|
1069
|
+
};
|
|
918
1070
|
const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
|
|
919
1071
|
const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
|
|
920
1072
|
const listOutputSchema = z.object({
|
|
@@ -927,7 +1079,21 @@ function createCrudRouter(config) {
|
|
|
927
1079
|
const metadataOutputSchema = z.object({
|
|
928
1080
|
schema: z.unknown().optional(),
|
|
929
1081
|
fields: z.record(z.string(), z.unknown()).optional(),
|
|
930
|
-
errors: z.array(z.string()).optional()
|
|
1082
|
+
errors: z.array(z.string()).optional(),
|
|
1083
|
+
capabilities: z.object({
|
|
1084
|
+
search: z.object({
|
|
1085
|
+
enabled: z.boolean(),
|
|
1086
|
+
fields: z.array(z.string())
|
|
1087
|
+
}),
|
|
1088
|
+
filters: z.object({
|
|
1089
|
+
enabled: z.boolean(),
|
|
1090
|
+
fields: z.array(z.string()).nullable()
|
|
1091
|
+
}),
|
|
1092
|
+
sort: z.object({
|
|
1093
|
+
enabled: z.boolean(),
|
|
1094
|
+
fields: z.array(z.string()).nullable()
|
|
1095
|
+
})
|
|
1096
|
+
})
|
|
931
1097
|
});
|
|
932
1098
|
const deleteManyOutputSchema = z.object({ deleted: z.number() });
|
|
933
1099
|
const updateManyOutputSchema = z.object({ updated: z.number() });
|
|
@@ -1004,10 +1170,10 @@ function createCrudRouter(config) {
|
|
|
1004
1170
|
meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
|
|
1005
1171
|
await withGuard(ctx, "list");
|
|
1006
1172
|
const target = readCrudTarget(config);
|
|
1007
|
-
if (!target) return {};
|
|
1173
|
+
if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
|
|
1008
1174
|
const provider = resolveCrudExtensions(ctx, config);
|
|
1009
|
-
if (!provider?.getMetadata) return {};
|
|
1010
|
-
return provider.getMetadata({ id: target.id });
|
|
1175
|
+
if (!provider?.getMetadata) return withCrudCapabilities({}, capabilities, disabledCapabilities);
|
|
1176
|
+
return withCrudCapabilities(await provider.getMetadata({ id: target.id }), capabilities, disabledCapabilities);
|
|
1011
1177
|
}),
|
|
1012
1178
|
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
1013
1179
|
await withGuard(ctx, "list");
|
|
@@ -1027,7 +1193,8 @@ function createCrudRouter(config) {
|
|
|
1027
1193
|
columnId: id,
|
|
1028
1194
|
allowedColumns: filterableColumns
|
|
1029
1195
|
});
|
|
1030
|
-
}
|
|
1196
|
+
},
|
|
1197
|
+
resolveDateRange: ctx.resolveDateRange
|
|
1031
1198
|
}) : void 0, buildCrudSearchCondition({
|
|
1032
1199
|
table,
|
|
1033
1200
|
search: effectiveInput.search,
|
|
@@ -1321,7 +1488,8 @@ function createCrudRouter(config) {
|
|
|
1321
1488
|
columnId: id,
|
|
1322
1489
|
allowedColumns: filterableColumns
|
|
1323
1490
|
});
|
|
1324
|
-
}
|
|
1491
|
+
},
|
|
1492
|
+
resolveDateRange: ctx.resolveDateRange
|
|
1325
1493
|
}) : void 0, buildCrudSearchCondition({
|
|
1326
1494
|
table,
|
|
1327
1495
|
search: effectiveInput.search,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/denvey/wordrhyme-components",
|
|
11
11
|
"directory": "packages/auto-crud-server"
|
|
12
12
|
},
|
|
13
13
|
"main": "./dist/index.cjs",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"vitest": "^4.0.18",
|
|
50
50
|
"zod": "^4.3.6",
|
|
51
51
|
"@internal/eslint-config": "0.3.0",
|
|
52
|
-
"@internal/tsconfig": "0.1.0",
|
|
53
|
-
"@internal/vitest-config": "0.1.0",
|
|
54
52
|
"@internal/tsdown-config": "0.1.0",
|
|
55
|
-
"@internal/
|
|
53
|
+
"@internal/tsconfig": "0.1.0",
|
|
54
|
+
"@internal/prettier-config": "0.0.1",
|
|
55
|
+
"@internal/vitest-config": "0.1.0"
|
|
56
56
|
},
|
|
57
57
|
"prettier": "@internal/prettier-config",
|
|
58
58
|
"scripts": {
|