@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 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 filterColumns({ table, filters, joinOperator, resolveColumn }) {
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 (!allowedColumns || allowedColumns.length === 0) return true;
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 ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
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 && allowedColumns.length > 0) return resolveColumnTarget({
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) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
862
- else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
863
- else extensionFilters.push(filter);
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 void 0;
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", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
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 filterColumns({ table, filters, joinOperator, resolveColumn }) {
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 (!allowedColumns || allowedColumns.length === 0) return true;
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 ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
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 && allowedColumns.length > 0) return resolveColumnTarget({
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) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
833
- else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
834
- else extensionFilters.push(filter);
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 void 0;
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", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
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.1.8",
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/pixpilot/shadcn-components.git",
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/prettier-config": "0.0.1"
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": {