@wordrhyme/auto-crud-server 1.1.8 → 1.2.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/dist/index.cjs CHANGED
@@ -621,12 +621,87 @@ function getColumnRefId(columnRef) {
621
621
  return typeof columnRef === "string" ? columnRef : columnRef.id;
622
622
  }
623
623
  function findColumnRef(columnId, allowedColumns) {
624
+ if (!Array.isArray(allowedColumns)) return void 0;
624
625
  return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
625
626
  }
626
627
  function validateColumn(columnId, allowedColumns) {
627
- if (!allowedColumns || allowedColumns.length === 0) return true;
628
+ if (allowedColumns === false) return false;
629
+ if (allowedColumns === void 0) return true;
628
630
  return findColumnRef(columnId, allowedColumns) !== void 0;
629
631
  }
632
+ function resolveColumnCapability(config, nextKey, legacyKey) {
633
+ const nextValue = config[nextKey];
634
+ const legacyValue = config[legacyKey];
635
+ if (nextValue !== void 0 && legacyValue !== void 0) throw new Error(`[createCrudRouter] "${nextKey}" and "${legacyKey}" cannot be used together.`);
636
+ const value = nextValue !== void 0 ? nextValue : legacyValue;
637
+ if (value === void 0) return void 0;
638
+ if (value === false) return false;
639
+ if (Array.isArray(value)) return value;
640
+ throw new Error(`[createCrudRouter] "${nextKey}" must be an array of column refs or false.`);
641
+ }
642
+ function readCapabilityFields(columns) {
643
+ if (columns === false) return [];
644
+ if (Array.isArray(columns)) return columns.map(getColumnRefId);
645
+ return null;
646
+ }
647
+ function buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns) {
648
+ const searchFields = readCapabilityFields(searchColumns) ?? [];
649
+ return {
650
+ search: {
651
+ enabled: searchFields.length > 0,
652
+ fields: searchFields
653
+ },
654
+ filters: {
655
+ enabled: filterableColumns !== false,
656
+ fields: readCapabilityFields(filterableColumns)
657
+ },
658
+ sort: {
659
+ enabled: sortableColumns !== false,
660
+ fields: readCapabilityFields(sortableColumns)
661
+ }
662
+ };
663
+ }
664
+ function uniqueCapabilityFields(...fields) {
665
+ return Array.from(new Set(fields.flatMap((fieldList) => fieldList ?? [])));
666
+ }
667
+ function mergeSearchCapability(base, extension, disabled) {
668
+ if (disabled) return {
669
+ enabled: false,
670
+ fields: []
671
+ };
672
+ const fields = uniqueCapabilityFields(base.fields, extension?.fields);
673
+ return {
674
+ enabled: base.enabled || extension?.enabled === true || fields.length > 0,
675
+ fields
676
+ };
677
+ }
678
+ function mergeFieldCapability(base, extension, disabled) {
679
+ if (disabled) return {
680
+ enabled: false,
681
+ fields: []
682
+ };
683
+ if (!(base.enabled || extension?.enabled === true)) return {
684
+ enabled: false,
685
+ fields: []
686
+ };
687
+ return {
688
+ enabled: true,
689
+ fields: base.fields === null || extension?.fields === null ? null : uniqueCapabilityFields(base.fields, extension?.fields)
690
+ };
691
+ }
692
+ function mergeCrudCapabilities(base, extension, disabled) {
693
+ return {
694
+ search: mergeSearchCapability(base.search, extension?.search, disabled.search),
695
+ filters: mergeFieldCapability(base.filters, extension?.filters, disabled.filters),
696
+ sort: mergeFieldCapability(base.sort, extension?.sort, disabled.sort)
697
+ };
698
+ }
699
+ function withCrudCapabilities(metadata, capabilities, disabled) {
700
+ return {
701
+ ...metadata ?? {},
702
+ capabilities: mergeCrudCapabilities(capabilities, metadata?.capabilities, disabled)
703
+ };
704
+ }
630
705
  function getTableColumn(table, columnId) {
631
706
  return table[columnId];
632
707
  }
@@ -645,7 +720,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
645
720
  }
646
721
  function resolveColumnTarget({ table, columnId, allowedColumns }) {
647
722
  const columnRef = findColumnRef(columnId, allowedColumns);
648
- if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
723
+ if (allowedColumns === false) return;
724
+ if (Array.isArray(allowedColumns) && columnRef === void 0) return;
649
725
  if (columnRef && typeof columnRef !== "string") {
650
726
  if (columnRef.expression) return columnRef.expression({ table });
651
727
  const column = getTableColumn(table, columnRef.id);
@@ -841,7 +917,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
841
917
  });
842
918
  }
843
919
  function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
844
- if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
920
+ if (allowedColumns === false) return false;
921
+ if (Array.isArray(allowedColumns)) return resolveColumnTarget({
845
922
  table,
846
923
  columnId,
847
924
  allowedColumns
@@ -856,11 +933,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
856
933
  const filters = input.filters ?? [];
857
934
  const search = typeof input.search === "string" ? input.search.trim() : "";
858
935
  if (!target || filters.length === 0 && !search) return input;
936
+ const filtersDisabled = filterableColumns === false;
937
+ const searchDisabled = searchColumns === false;
859
938
  const baseFilters = [];
860
939
  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);
940
+ for (const filter of filters) {
941
+ if (filtersDisabled) continue;
942
+ if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
943
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
944
+ else extensionFilters.push(filter);
945
+ }
864
946
  if (extensionFilters.length === 0 && !search) return {
865
947
  ...input,
866
948
  filters: baseFilters
@@ -880,14 +962,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
880
962
  });
881
963
  matchedSets.push(new Set(matchedIds$1));
882
964
  }
883
- if (search && provider?.searchEntityIds) {
965
+ if (search && !searchDisabled && provider?.searchEntityIds) {
884
966
  const matchedIds$1 = await provider.searchEntityIds({
885
967
  id: target.id,
886
968
  search,
887
969
  limit: 5e3
888
970
  });
889
971
  matchedSets.push(new Set(matchedIds$1));
890
- } else if (search && !buildCrudSearchCondition({
972
+ } else if (search && !searchDisabled && !buildCrudSearchCondition({
891
973
  table,
892
974
  search,
893
975
  searchColumns
@@ -914,7 +996,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
914
996
  }
915
997
  function buildCrudSearchCondition({ table, search, searchColumns }) {
916
998
  const term = typeof search === "string" ? search.trim() : "";
917
- if (!term || !searchColumns || searchColumns.length === 0) return void 0;
999
+ if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
918
1000
  const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
919
1001
  table,
920
1002
  columnId: getColumnRefId(columnRef),
@@ -943,7 +1025,16 @@ function combineConditions(...conditions) {
943
1025
  * @typeParam TUpdate - 更新输入类型
944
1026
  */
945
1027
  function createCrudRouter(config) {
946
- const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
1028
+ const { table, idField = "id", maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
1029
+ const searchColumns = resolveColumnCapability(config, "search", "searchColumns");
1030
+ const filterableColumns = resolveColumnCapability(config, "filters", "filterableColumns");
1031
+ const sortableColumns = resolveColumnCapability(config, "sort", "sortableColumns");
1032
+ const capabilities = buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns);
1033
+ const disabledCapabilities = {
1034
+ search: searchColumns === false,
1035
+ filters: filterableColumns === false,
1036
+ sort: sortableColumns === false
1037
+ };
947
1038
  const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
948
1039
  const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
949
1040
  const listOutputSchema = zod.z.object({
@@ -956,7 +1047,21 @@ function createCrudRouter(config) {
956
1047
  const metadataOutputSchema = zod.z.object({
957
1048
  schema: zod.z.unknown().optional(),
958
1049
  fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
959
- errors: zod.z.array(zod.z.string()).optional()
1050
+ errors: zod.z.array(zod.z.string()).optional(),
1051
+ capabilities: zod.z.object({
1052
+ search: zod.z.object({
1053
+ enabled: zod.z.boolean(),
1054
+ fields: zod.z.array(zod.z.string())
1055
+ }),
1056
+ filters: zod.z.object({
1057
+ enabled: zod.z.boolean(),
1058
+ fields: zod.z.array(zod.z.string()).nullable()
1059
+ }),
1060
+ sort: zod.z.object({
1061
+ enabled: zod.z.boolean(),
1062
+ fields: zod.z.array(zod.z.string()).nullable()
1063
+ })
1064
+ })
960
1065
  });
961
1066
  const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
962
1067
  const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
@@ -1033,10 +1138,10 @@ function createCrudRouter(config) {
1033
1138
  meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
1034
1139
  await withGuard(ctx, "list");
1035
1140
  const target = readCrudTarget(config);
1036
- if (!target) return {};
1141
+ if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
1037
1142
  const provider = resolveCrudExtensions(ctx, config);
1038
- if (!provider?.getMetadata) return {};
1039
- return provider.getMetadata({ id: target.id });
1143
+ if (!provider?.getMetadata) return withCrudCapabilities({}, capabilities, disabledCapabilities);
1144
+ return withCrudCapabilities(await provider.getMetadata({ id: target.id }), capabilities, disabledCapabilities);
1040
1145
  }),
1041
1146
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
1042
1147
  await withGuard(ctx, "list");
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { PgTable } from "drizzle-orm/pg-core";
3
- import * as _trpc_server2 from "@trpc/server";
3
+ import * as _trpc_server0 from "@trpc/server";
4
4
  import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
5
5
  import { AnyColumn, SQL } from "drizzle-orm";
6
6
 
@@ -12,13 +12,13 @@ import { AnyColumn, SQL } from "drizzle-orm";
12
12
  interface Context {
13
13
  db: PostgresJsDatabase<Record<string, never>>;
14
14
  }
15
- declare const router: _trpc_server2.TRPCRouterBuilder<{
15
+ declare const router: _trpc_server0.TRPCRouterBuilder<{
16
16
  ctx: Context;
17
17
  meta: object;
18
- errorShape: _trpc_server2.TRPCDefaultErrorShape;
18
+ errorShape: _trpc_server0.TRPCDefaultErrorShape;
19
19
  transformer: true;
20
20
  }>;
21
- declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
21
+ declare const publicProcedure: _trpc_server0.TRPCProcedureBuilder<Context, object, object, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, false>;
22
22
  //#endregion
23
23
  //#region src/types/config.d.ts
24
24
  type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
@@ -54,6 +54,21 @@ interface CrudColumnConfig<TTable extends PgTable = PgTable> {
54
54
  expression?: CrudColumnExpression<TTable>;
55
55
  }
56
56
  type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
57
+ type CrudColumnCapability<TTable extends PgTable = PgTable> = false | CrudColumnRef<TTable>[];
58
+ interface CrudCapabilityMetadata {
59
+ search: {
60
+ enabled: boolean;
61
+ fields: string[];
62
+ };
63
+ filters: {
64
+ enabled: boolean;
65
+ fields: string[] | null;
66
+ };
67
+ sort: {
68
+ enabled: boolean;
69
+ fields: string[] | null;
70
+ };
71
+ }
57
72
  interface SoftDeleteConfig {
58
73
  /** 软删除标记列名 */
59
74
  column: string;
@@ -133,6 +148,8 @@ interface CrudExtensionMetadata {
133
148
  schema?: unknown;
134
149
  fields?: Record<string, unknown>;
135
150
  errors?: string[];
151
+ /** 扩展提供的查询能力,会与 router 基础能力合并。 */
152
+ capabilities?: CrudCapabilityMetadata;
136
153
  }
137
154
  interface CrudExtensionFilter {
138
155
  id: string;
@@ -586,9 +603,34 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
586
603
  omitFields?: TableColumnKeys<TTable>[];
587
604
  /** ID 字段名,默认 "id" */
588
605
  idField?: string;
606
+ /**
607
+ * 全局搜索列白名单。
608
+ *
609
+ * 不配置时不启用全局搜索;传数组时启用并只搜索指定字段;传 false 显式禁用。
610
+ *
611
+ * @example
612
+ * search: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
613
+ */
614
+ search?: CrudColumnCapability<TTable>;
615
+ /**
616
+ * 可过滤列白名单。
617
+ *
618
+ * 不配置时保持兼容行为:所有真实表字段可过滤;传数组时只允许指定字段;
619
+ * 传 false 显式禁用过滤。
620
+ */
621
+ filters?: CrudColumnCapability<TTable>;
622
+ /**
623
+ * 可排序列白名单。
624
+ *
625
+ * 不配置时保持兼容行为:所有真实表字段可排序;传数组时只允许指定字段;
626
+ * 传 false 显式禁用排序。
627
+ */
628
+ sort?: CrudColumnCapability<TTable>;
589
629
  /**
590
630
  * 可过滤的列白名单。
591
631
  *
632
+ * @deprecated Use `filters` instead.
633
+ *
592
634
  * 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
593
635
  * 由服务端显式生成 text expression,前端仍然传 id。
594
636
  *
@@ -602,6 +644,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
602
644
  /**
603
645
  * 全局搜索列白名单。
604
646
  *
647
+ * @deprecated Use `search` instead.
648
+ *
605
649
  * 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
606
650
  * 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
607
651
  * `CrudExtensionsProvider.searchEntityIds` 处理。
@@ -613,6 +657,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
613
657
  /**
614
658
  * 可排序的列白名单。
615
659
  *
660
+ * @deprecated Use `sort` instead.
661
+ *
616
662
  * 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
617
663
  */
618
664
  sortableColumns?: CrudColumnRef<TTable>[];
@@ -890,12 +936,12 @@ type CrudHookEventMap<PluginId extends string, ResourceName extends string, TCre
890
936
  } } & { [K in `${PluginId}.${ResourceName}.createMany`]: TCreate[] };
891
937
  //#endregion
892
938
  //#region src/routers/index.d.ts
893
- declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
939
+ declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
894
940
  ctx: Context;
895
941
  meta: object;
896
- errorShape: _trpc_server2.TRPCDefaultErrorShape;
942
+ errorShape: _trpc_server0.TRPCDefaultErrorShape;
897
943
  transformer: true;
898
- }, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
944
+ }, _trpc_server0.TRPCDecorateCreateRouterOptions<{}>>;
899
945
  type AppRouter = typeof appRouter;
900
946
  //#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 };
947
+ 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 DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AnyColumn, SQL } from "drizzle-orm";
3
- import * as _trpc_server2 from "@trpc/server";
3
+ import * as _trpc_server0 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";
@@ -13,13 +13,13 @@ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
13
13
  interface Context {
14
14
  db: PostgresJsDatabase<Record<string, never>>;
15
15
  }
16
- declare const router: _trpc_server2.TRPCRouterBuilder<{
16
+ declare const router: _trpc_server0.TRPCRouterBuilder<{
17
17
  ctx: Context;
18
18
  meta: object;
19
- errorShape: _trpc_server2.TRPCDefaultErrorShape;
19
+ errorShape: _trpc_server0.TRPCDefaultErrorShape;
20
20
  transformer: true;
21
21
  }>;
22
- declare const publicProcedure: _trpc_server2.TRPCProcedureBuilder<Context, object, object, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, _trpc_server2.TRPCUnsetMarker, false>;
22
+ declare const publicProcedure: _trpc_server0.TRPCProcedureBuilder<Context, object, object, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, false>;
23
23
  //#endregion
24
24
  //#region src/types/config.d.ts
25
25
  type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
@@ -55,6 +55,21 @@ interface CrudColumnConfig<TTable extends PgTable = PgTable> {
55
55
  expression?: CrudColumnExpression<TTable>;
56
56
  }
57
57
  type CrudColumnRef<TTable extends PgTable = PgTable> = string | CrudColumnConfig<TTable>;
58
+ type CrudColumnCapability<TTable extends PgTable = PgTable> = false | CrudColumnRef<TTable>[];
59
+ interface CrudCapabilityMetadata {
60
+ search: {
61
+ enabled: boolean;
62
+ fields: string[];
63
+ };
64
+ filters: {
65
+ enabled: boolean;
66
+ fields: string[] | null;
67
+ };
68
+ sort: {
69
+ enabled: boolean;
70
+ fields: string[] | null;
71
+ };
72
+ }
58
73
  interface SoftDeleteConfig {
59
74
  /** 软删除标记列名 */
60
75
  column: string;
@@ -134,6 +149,8 @@ interface CrudExtensionMetadata {
134
149
  schema?: unknown;
135
150
  fields?: Record<string, unknown>;
136
151
  errors?: string[];
152
+ /** 扩展提供的查询能力,会与 router 基础能力合并。 */
153
+ capabilities?: CrudCapabilityMetadata;
137
154
  }
138
155
  interface CrudExtensionFilter {
139
156
  id: string;
@@ -587,9 +604,34 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
587
604
  omitFields?: TableColumnKeys<TTable>[];
588
605
  /** ID 字段名,默认 "id" */
589
606
  idField?: string;
607
+ /**
608
+ * 全局搜索列白名单。
609
+ *
610
+ * 不配置时不启用全局搜索;传数组时启用并只搜索指定字段;传 false 显式禁用。
611
+ *
612
+ * @example
613
+ * search: ['title', { id: 'name', jsonField: ['zh-CN', 'en-US'] }]
614
+ */
615
+ search?: CrudColumnCapability<TTable>;
616
+ /**
617
+ * 可过滤列白名单。
618
+ *
619
+ * 不配置时保持兼容行为:所有真实表字段可过滤;传数组时只允许指定字段;
620
+ * 传 false 显式禁用过滤。
621
+ */
622
+ filters?: CrudColumnCapability<TTable>;
623
+ /**
624
+ * 可排序列白名单。
625
+ *
626
+ * 不配置时保持兼容行为:所有真实表字段可排序;传数组时只允许指定字段;
627
+ * 传 false 显式禁用排序。
628
+ */
629
+ sort?: CrudColumnCapability<TTable>;
590
630
  /**
591
631
  * 可过滤的列白名单。
592
632
  *
633
+ * @deprecated Use `filters` instead.
634
+ *
593
635
  * 普通字段使用字符串;jsonb/i18n 字段可用对象配置 jsonField,
594
636
  * 由服务端显式生成 text expression,前端仍然传 id。
595
637
  *
@@ -603,6 +645,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
603
645
  /**
604
646
  * 全局搜索列白名单。
605
647
  *
648
+ * @deprecated Use `search` instead.
649
+ *
606
650
  * 当列表/导出输入包含 `search` 时,默认查询会对这些列做文本化 ILIKE
607
651
  * 搜索,并与其他筛选条件按 AND 组合。扩展字段搜索仍由
608
652
  * `CrudExtensionsProvider.searchEntityIds` 处理。
@@ -614,6 +658,8 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
614
658
  /**
615
659
  * 可排序的列白名单。
616
660
  *
661
+ * @deprecated Use `sort` instead.
662
+ *
617
663
  * 与 filterableColumns 相同,普通字段使用字符串,jsonb/i18n 字段可配置 jsonField。
618
664
  */
619
665
  sortableColumns?: CrudColumnRef<TTable>[];
@@ -891,12 +937,12 @@ type CrudHookEventMap<PluginId extends string, ResourceName extends string, TCre
891
937
  } } & { [K in `${PluginId}.${ResourceName}.createMany`]: TCreate[] };
892
938
  //#endregion
893
939
  //#region src/routers/index.d.ts
894
- declare const appRouter: _trpc_server2.TRPCBuiltRouter<{
940
+ declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
895
941
  ctx: Context;
896
942
  meta: object;
897
- errorShape: _trpc_server2.TRPCDefaultErrorShape;
943
+ errorShape: _trpc_server0.TRPCDefaultErrorShape;
898
944
  transformer: true;
899
- }, _trpc_server2.TRPCDecorateCreateRouterOptions<{}>>;
945
+ }, _trpc_server0.TRPCDecorateCreateRouterOptions<{}>>;
900
946
  type AppRouter = typeof appRouter;
901
947
  //#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 };
948
+ 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 DeleteManyMiddlewareParams, type DeleteMiddlewareParams, type ExportInput, type ExportMiddlewareParams, type ExportResult, type GetInput, type GetMiddlewareParams, type ImportFailedRow, type ImportInput, type ImportMiddlewareParams, type ImportResult, type ListInput, type ListMiddlewareParams, type ListResult, type ProcedureConfig, type ProcedureFactory, type ProcedureMap, type SoftDeleteConfig, type SoftDeleteOption, type UpdateManyMiddlewareParams, type UpdateMiddlewareParams, type UpsertMiddlewareParams, type WriteOperation, afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
package/dist/index.js CHANGED
@@ -592,12 +592,87 @@ function getColumnRefId(columnRef) {
592
592
  return typeof columnRef === "string" ? columnRef : columnRef.id;
593
593
  }
594
594
  function findColumnRef(columnId, allowedColumns) {
595
+ if (!Array.isArray(allowedColumns)) return void 0;
595
596
  return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
596
597
  }
597
598
  function validateColumn(columnId, allowedColumns) {
598
- if (!allowedColumns || allowedColumns.length === 0) return true;
599
+ if (allowedColumns === false) return false;
600
+ if (allowedColumns === void 0) return true;
599
601
  return findColumnRef(columnId, allowedColumns) !== void 0;
600
602
  }
603
+ function resolveColumnCapability(config, nextKey, legacyKey) {
604
+ const nextValue = config[nextKey];
605
+ const legacyValue = config[legacyKey];
606
+ if (nextValue !== void 0 && legacyValue !== void 0) throw new Error(`[createCrudRouter] "${nextKey}" and "${legacyKey}" cannot be used together.`);
607
+ const value = nextValue !== void 0 ? nextValue : legacyValue;
608
+ if (value === void 0) return void 0;
609
+ if (value === false) return false;
610
+ if (Array.isArray(value)) return value;
611
+ throw new Error(`[createCrudRouter] "${nextKey}" must be an array of column refs or false.`);
612
+ }
613
+ function readCapabilityFields(columns) {
614
+ if (columns === false) return [];
615
+ if (Array.isArray(columns)) return columns.map(getColumnRefId);
616
+ return null;
617
+ }
618
+ function buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns) {
619
+ const searchFields = readCapabilityFields(searchColumns) ?? [];
620
+ return {
621
+ search: {
622
+ enabled: searchFields.length > 0,
623
+ fields: searchFields
624
+ },
625
+ filters: {
626
+ enabled: filterableColumns !== false,
627
+ fields: readCapabilityFields(filterableColumns)
628
+ },
629
+ sort: {
630
+ enabled: sortableColumns !== false,
631
+ fields: readCapabilityFields(sortableColumns)
632
+ }
633
+ };
634
+ }
635
+ function uniqueCapabilityFields(...fields) {
636
+ return Array.from(new Set(fields.flatMap((fieldList) => fieldList ?? [])));
637
+ }
638
+ function mergeSearchCapability(base, extension, disabled) {
639
+ if (disabled) return {
640
+ enabled: false,
641
+ fields: []
642
+ };
643
+ const fields = uniqueCapabilityFields(base.fields, extension?.fields);
644
+ return {
645
+ enabled: base.enabled || extension?.enabled === true || fields.length > 0,
646
+ fields
647
+ };
648
+ }
649
+ function mergeFieldCapability(base, extension, disabled) {
650
+ if (disabled) return {
651
+ enabled: false,
652
+ fields: []
653
+ };
654
+ if (!(base.enabled || extension?.enabled === true)) return {
655
+ enabled: false,
656
+ fields: []
657
+ };
658
+ return {
659
+ enabled: true,
660
+ fields: base.fields === null || extension?.fields === null ? null : uniqueCapabilityFields(base.fields, extension?.fields)
661
+ };
662
+ }
663
+ function mergeCrudCapabilities(base, extension, disabled) {
664
+ return {
665
+ search: mergeSearchCapability(base.search, extension?.search, disabled.search),
666
+ filters: mergeFieldCapability(base.filters, extension?.filters, disabled.filters),
667
+ sort: mergeFieldCapability(base.sort, extension?.sort, disabled.sort)
668
+ };
669
+ }
670
+ function withCrudCapabilities(metadata, capabilities, disabled) {
671
+ return {
672
+ ...metadata ?? {},
673
+ capabilities: mergeCrudCapabilities(capabilities, metadata?.capabilities, disabled)
674
+ };
675
+ }
601
676
  function getTableColumn(table, columnId) {
602
677
  return table[columnId];
603
678
  }
@@ -616,7 +691,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
616
691
  }
617
692
  function resolveColumnTarget({ table, columnId, allowedColumns }) {
618
693
  const columnRef = findColumnRef(columnId, allowedColumns);
619
- if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
694
+ if (allowedColumns === false) return;
695
+ if (Array.isArray(allowedColumns) && columnRef === void 0) return;
620
696
  if (columnRef && typeof columnRef !== "string") {
621
697
  if (columnRef.expression) return columnRef.expression({ table });
622
698
  const column = getTableColumn(table, columnRef.id);
@@ -812,7 +888,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
812
888
  });
813
889
  }
814
890
  function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
815
- if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
891
+ if (allowedColumns === false) return false;
892
+ if (Array.isArray(allowedColumns)) return resolveColumnTarget({
816
893
  table,
817
894
  columnId,
818
895
  allowedColumns
@@ -827,11 +904,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
827
904
  const filters = input.filters ?? [];
828
905
  const search = typeof input.search === "string" ? input.search.trim() : "";
829
906
  if (!target || filters.length === 0 && !search) return input;
907
+ const filtersDisabled = filterableColumns === false;
908
+ const searchDisabled = searchColumns === false;
830
909
  const baseFilters = [];
831
910
  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);
911
+ for (const filter of filters) {
912
+ if (filtersDisabled) continue;
913
+ if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
914
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
915
+ else extensionFilters.push(filter);
916
+ }
835
917
  if (extensionFilters.length === 0 && !search) return {
836
918
  ...input,
837
919
  filters: baseFilters
@@ -851,14 +933,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
851
933
  });
852
934
  matchedSets.push(new Set(matchedIds$1));
853
935
  }
854
- if (search && provider?.searchEntityIds) {
936
+ if (search && !searchDisabled && provider?.searchEntityIds) {
855
937
  const matchedIds$1 = await provider.searchEntityIds({
856
938
  id: target.id,
857
939
  search,
858
940
  limit: 5e3
859
941
  });
860
942
  matchedSets.push(new Set(matchedIds$1));
861
- } else if (search && !buildCrudSearchCondition({
943
+ } else if (search && !searchDisabled && !buildCrudSearchCondition({
862
944
  table,
863
945
  search,
864
946
  searchColumns
@@ -885,7 +967,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
885
967
  }
886
968
  function buildCrudSearchCondition({ table, search, searchColumns }) {
887
969
  const term = typeof search === "string" ? search.trim() : "";
888
- if (!term || !searchColumns || searchColumns.length === 0) return void 0;
970
+ if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
889
971
  const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
890
972
  table,
891
973
  columnId: getColumnRefId(columnRef),
@@ -914,7 +996,16 @@ function combineConditions(...conditions) {
914
996
  * @typeParam TUpdate - 更新输入类型
915
997
  */
916
998
  function createCrudRouter(config) {
917
- const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
999
+ const { table, idField = "id", maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
1000
+ const searchColumns = resolveColumnCapability(config, "search", "searchColumns");
1001
+ const filterableColumns = resolveColumnCapability(config, "filters", "filterableColumns");
1002
+ const sortableColumns = resolveColumnCapability(config, "sort", "sortableColumns");
1003
+ const capabilities = buildCrudCapabilities(searchColumns, filterableColumns, sortableColumns);
1004
+ const disabledCapabilities = {
1005
+ search: searchColumns === false,
1006
+ filters: filterableColumns === false,
1007
+ sort: sortableColumns === false
1008
+ };
918
1009
  const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
919
1010
  const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
920
1011
  const listOutputSchema = z.object({
@@ -927,7 +1018,21 @@ function createCrudRouter(config) {
927
1018
  const metadataOutputSchema = z.object({
928
1019
  schema: z.unknown().optional(),
929
1020
  fields: z.record(z.string(), z.unknown()).optional(),
930
- errors: z.array(z.string()).optional()
1021
+ errors: z.array(z.string()).optional(),
1022
+ capabilities: z.object({
1023
+ search: z.object({
1024
+ enabled: z.boolean(),
1025
+ fields: z.array(z.string())
1026
+ }),
1027
+ filters: z.object({
1028
+ enabled: z.boolean(),
1029
+ fields: z.array(z.string()).nullable()
1030
+ }),
1031
+ sort: z.object({
1032
+ enabled: z.boolean(),
1033
+ fields: z.array(z.string()).nullable()
1034
+ })
1035
+ })
931
1036
  });
932
1037
  const deleteManyOutputSchema = z.object({ deleted: z.number() });
933
1038
  const updateManyOutputSchema = z.object({ updated: z.number() });
@@ -1004,10 +1109,10 @@ function createCrudRouter(config) {
1004
1109
  meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
1005
1110
  await withGuard(ctx, "list");
1006
1111
  const target = readCrudTarget(config);
1007
- if (!target) return {};
1112
+ if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
1008
1113
  const provider = resolveCrudExtensions(ctx, config);
1009
- if (!provider?.getMetadata) return {};
1010
- return provider.getMetadata({ id: target.id });
1114
+ if (!provider?.getMetadata) return withCrudCapabilities({}, capabilities, disabledCapabilities);
1115
+ return withCrudCapabilities(await provider.getMetadata({ id: target.id }), capabilities, disabledCapabilities);
1011
1116
  }),
1012
1117
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
1013
1118
  await withGuard(ctx, "list");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud-server",
3
3
  "type": "module",
4
- "version": "1.1.8",
4
+ "version": "1.2.0",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -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/prettier-config": "0.0.1",
52
53
  "@internal/tsconfig": "0.1.0",
53
- "@internal/vitest-config": "0.1.0",
54
54
  "@internal/tsdown-config": "0.1.0",
55
- "@internal/prettier-config": "0.0.1"
55
+ "@internal/vitest-config": "0.1.0"
56
56
  },
57
57
  "prettier": "@internal/prettier-config",
58
58
  "scripts": {