@wordrhyme/auto-crud-server 1.1.6 → 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/README.md CHANGED
@@ -83,7 +83,7 @@ import { tasks } from '@/db/schema';
83
83
  // 🚀 零配置!一行代码生成完整 CRUD 路由
84
84
  export const tasksRouter = createCrudRouter({
85
85
  table: tasks,
86
- // Schema 自动从 table 派生,排除 id, createdAt, updatedAt
86
+ // Schema 自动从 table 派生,排除平台托管字段
87
87
  });
88
88
  ```
89
89
 
@@ -169,6 +169,9 @@ export { handler as GET, handler as POST };
169
169
  }
170
170
  ```
171
171
 
172
+ 未传 `sort` 或传空数组时,如果表存在 `createdAt` 且未被 `sortableColumns`
173
+ 白名单排除,列表默认按 `createdAt` 降序返回。
174
+
172
175
  **输出**:
173
176
 
174
177
  ```typescript
@@ -491,18 +494,18 @@ interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
491
494
  idField?: string; // ID 字段名,默认 "id"
492
495
  filterableColumns?: CrudColumnRef<TTable>[]; // 可过滤字段白名单
493
496
  sortableColumns?: CrudColumnRef<TTable>[]; // 可排序字段白名单
494
- omitFields?: string[]; // 自动派生时排除的字段
495
- // 默认 ["id", "createdAt", "updatedAt"]
497
+ omitFields?: string[]; // 自动派生时额外排除的业务字段
498
+ // 默认已排除 ["id", "createdAt", "updatedAt", "createdBy", "createdByType", "updatedBy", "updatedByType"]
496
499
  }
497
500
  ```
498
501
 
499
502
  #### Schema 派生规则
500
503
 
501
- | 配置 | 派生行为 |
502
- | ----------------- | ------------------------------------------- |
503
- | 无 `schema` | 从 `table` 自动派生,排除 `omitFields` |
504
- | 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
505
- | 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
504
+ | 配置 | 派生行为 |
505
+ | ----------------- | ---------------------------------------------------- |
506
+ | 无 `schema` | 从 `table` 自动派生,排除默认托管字段和 `omitFields` |
507
+ | 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
508
+ | 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
506
509
 
507
510
  #### 使用示例
508
511
 
@@ -528,7 +531,7 @@ const usersRouter = createCrudRouter({
528
531
  // 4. 自定义排除字段
529
532
  const ordersRouter = createCrudRouter({
530
533
  table: orders,
531
- omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
534
+ omitFields: ['internalCode'],
532
535
  });
533
536
  ```
534
537
 
@@ -992,8 +995,8 @@ export const usersRouter = createCrudRouter({
992
995
  ```typescript
993
996
  export const ordersRouter = createCrudRouter({
994
997
  table: orders,
995
- // 自动派生 schema 时排除这些字段
996
- omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
998
+ // 自动派生 schema 时额外排除这些字段;默认托管字段无需重复写
999
+ omitFields: ['internalCode'],
997
1000
  });
998
1001
  ```
999
1002
 
package/dist/index.cjs CHANGED
@@ -562,7 +562,11 @@ const importInputSchema = zod.z.object({
562
562
  const DEFAULT_OMIT_FIELDS = [
563
563
  "id",
564
564
  "createdAt",
565
- "updatedAt"
565
+ "updatedAt",
566
+ "createdBy",
567
+ "createdByType",
568
+ "updatedBy",
569
+ "updatedByType"
566
570
  ];
567
571
  function shouldEnableCrudExtensionSchema(config) {
568
572
  return Boolean(config.id) && config.extensions !== false;
@@ -576,19 +580,30 @@ function withCrudExtensionInput(schema, config) {
576
580
  * - 优先使用显式传入的 Schema
577
581
  * - 否则从 Drizzle table 自动派生
578
582
  */
579
- function resolveSchemas(config) {
580
- const { table, omitFields = DEFAULT_OMIT_FIELDS } = config;
583
+ function resolveSchemas(config, idField = "id") {
584
+ const { table } = config;
585
+ const omitFields = [...new Set([...DEFAULT_OMIT_FIELDS, ...config.omitFields ?? []])];
581
586
  let schema;
587
+ let upsertSchema;
588
+ let importSchema;
582
589
  if (config.schema) {
583
590
  if (!isZodObject(config.schema)) throw new Error("[createCrudRouter] schema must be a ZodObject (not ZodEffects or other types). If you're using .refine() or .transform(), please remove them from schema.");
584
591
  schema = config.schema;
592
+ upsertSchema = schema;
593
+ importSchema = schema;
585
594
  } else {
586
595
  const rawSchema = (0, drizzle_zod.createInsertSchema)(table);
587
596
  if (!isZodObject(rawSchema)) throw new Error("[createCrudRouter] drizzle-zod returned non-ZodObject schema. This may be due to table refinements. Please provide schema explicitly.");
588
- const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
597
+ const omitConfig = Object.fromEntries(omitFields.filter((field) => field in rawSchema.shape).map((field) => [field, true]));
589
598
  schema = rawSchema.omit(omitConfig);
599
+ const idSchema = rawSchema.shape[idField];
600
+ const schemaWithOptionalId = idSchema && !(idField in schema.shape) ? schema.extend({ [idField]: idSchema.optional() }) : schema;
601
+ upsertSchema = schemaWithOptionalId;
602
+ importSchema = schemaWithOptionalId;
590
603
  }
591
604
  schema = withCrudExtensionInput(schema, config);
605
+ upsertSchema = withCrudExtensionInput(upsertSchema, config);
606
+ importSchema = withCrudExtensionInput(importSchema, config);
592
607
  let selectSchema;
593
608
  if (config.selectSchema) selectSchema = config.selectSchema;
594
609
  else if (config.schema) selectSchema = schema;
@@ -597,6 +612,8 @@ function resolveSchemas(config) {
597
612
  return {
598
613
  selectSchema,
599
614
  schema,
615
+ upsertSchema,
616
+ importSchema,
600
617
  updateSchema
601
618
  };
602
619
  }
@@ -604,12 +621,87 @@ function getColumnRefId(columnRef) {
604
621
  return typeof columnRef === "string" ? columnRef : columnRef.id;
605
622
  }
606
623
  function findColumnRef(columnId, allowedColumns) {
624
+ if (!Array.isArray(allowedColumns)) return void 0;
607
625
  return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
608
626
  }
609
627
  function validateColumn(columnId, allowedColumns) {
610
- if (!allowedColumns || allowedColumns.length === 0) return true;
628
+ if (allowedColumns === false) return false;
629
+ if (allowedColumns === void 0) return true;
611
630
  return findColumnRef(columnId, allowedColumns) !== void 0;
612
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
+ }
613
705
  function getTableColumn(table, columnId) {
614
706
  return table[columnId];
615
707
  }
@@ -628,7 +720,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
628
720
  }
629
721
  function resolveColumnTarget({ table, columnId, allowedColumns }) {
630
722
  const columnRef = findColumnRef(columnId, allowedColumns);
631
- if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
723
+ if (allowedColumns === false) return;
724
+ if (Array.isArray(allowedColumns) && columnRef === void 0) return;
632
725
  if (columnRef && typeof columnRef !== "string") {
633
726
  if (columnRef.expression) return columnRef.expression({ table });
634
727
  const column = getTableColumn(table, columnRef.id);
@@ -824,7 +917,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
824
917
  });
825
918
  }
826
919
  function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
827
- if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
920
+ if (allowedColumns === false) return false;
921
+ if (Array.isArray(allowedColumns)) return resolveColumnTarget({
828
922
  table,
829
923
  columnId,
830
924
  allowedColumns
@@ -839,11 +933,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
839
933
  const filters = input.filters ?? [];
840
934
  const search = typeof input.search === "string" ? input.search.trim() : "";
841
935
  if (!target || filters.length === 0 && !search) return input;
936
+ const filtersDisabled = filterableColumns === false;
937
+ const searchDisabled = searchColumns === false;
842
938
  const baseFilters = [];
843
939
  const extensionFilters = [];
844
- for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
845
- else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
846
- 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
+ }
847
946
  if (extensionFilters.length === 0 && !search) return {
848
947
  ...input,
849
948
  filters: baseFilters
@@ -863,14 +962,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
863
962
  });
864
963
  matchedSets.push(new Set(matchedIds$1));
865
964
  }
866
- if (search && provider?.searchEntityIds) {
965
+ if (search && !searchDisabled && provider?.searchEntityIds) {
867
966
  const matchedIds$1 = await provider.searchEntityIds({
868
967
  id: target.id,
869
968
  search,
870
969
  limit: 5e3
871
970
  });
872
971
  matchedSets.push(new Set(matchedIds$1));
873
- } else if (search && !buildCrudSearchCondition({
972
+ } else if (search && !searchDisabled && !buildCrudSearchCondition({
874
973
  table,
875
974
  search,
876
975
  searchColumns
@@ -897,7 +996,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
897
996
  }
898
997
  function buildCrudSearchCondition({ table, search, searchColumns }) {
899
998
  const term = typeof search === "string" ? search.trim() : "";
900
- if (!term || !searchColumns || searchColumns.length === 0) return void 0;
999
+ if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
901
1000
  const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
902
1001
  table,
903
1002
  columnId: getColumnRefId(columnRef),
@@ -926,8 +1025,17 @@ function combineConditions(...conditions) {
926
1025
  * @typeParam TUpdate - 更新输入类型
927
1026
  */
928
1027
  function createCrudRouter(config) {
929
- const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
930
- const { selectSchema, schema, updateSchema } = resolveSchemas(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
+ };
1038
+ const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
931
1039
  const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
932
1040
  const listOutputSchema = zod.z.object({
933
1041
  data: zod.z.array(entityOutputSchema),
@@ -939,7 +1047,21 @@ function createCrudRouter(config) {
939
1047
  const metadataOutputSchema = zod.z.object({
940
1048
  schema: zod.z.unknown().optional(),
941
1049
  fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
942
- 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
+ })
943
1065
  });
944
1066
  const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
945
1067
  const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
@@ -1016,10 +1138,10 @@ function createCrudRouter(config) {
1016
1138
  meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
1017
1139
  await withGuard(ctx, "list");
1018
1140
  const target = readCrudTarget(config);
1019
- if (!target) return {};
1141
+ if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
1020
1142
  const provider = resolveCrudExtensions(ctx, config);
1021
- if (!provider?.getMetadata) return {};
1022
- 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);
1023
1145
  }),
1024
1146
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
1025
1147
  await withGuard(ctx, "list");
@@ -1047,9 +1169,12 @@ function createCrudRouter(config) {
1047
1169
  })));
1048
1170
  let query = ctx.db.select().from(table).$dynamic();
1049
1171
  if (where) query = query.where(where);
1050
- if (effectiveInput.sort?.length) {
1051
- const sortField = effectiveInput.sort[0];
1052
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
1172
+ const sortField = effectiveInput.sort?.[0] ?? (getTableColumn(table, "createdAt") ? {
1173
+ id: "createdAt",
1174
+ desc: true
1175
+ } : void 0);
1176
+ if (sortField) {
1177
+ if (validateColumn(sortField.id, sortableColumns)) {
1053
1178
  const column = resolveColumnTarget({
1054
1179
  table,
1055
1180
  columnId: sortField.id,
@@ -1255,7 +1380,7 @@ function createCrudRouter(config) {
1255
1380
  });
1256
1381
  return doUpdateMany(input.data);
1257
1382
  }),
1258
- upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
1383
+ upsert: resolved.procedureFactory("upsert").input(upsertSchema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
1259
1384
  await withGuard(ctx, "upsert");
1260
1385
  const doUpsert = async (inputData) => {
1261
1386
  const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
@@ -1400,7 +1525,7 @@ function createCrudRouter(config) {
1400
1525
  const failed = [];
1401
1526
  for (let i = 0; i < importData$1.rows.length; i++) {
1402
1527
  const row = importData$1.rows[i];
1403
- const result = schema.safeParse(row);
1528
+ const result = importSchema.safeParse(row);
1404
1529
  if (result.success) {
1405
1530
  const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1406
1531
  validRows.push(splitRow);
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;
@@ -581,14 +598,39 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
581
598
  authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
582
599
  /**
583
600
  * 要从 schema 排除的字段(自动派生时使用)
584
- * @default ["id", "createdAt", "updatedAt"]
601
+ * @default ["id", "createdAt", "updatedAt", "createdBy", "createdByType", "updatedBy", "updatedByType"]
585
602
  */
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
@@ -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;
@@ -582,14 +599,39 @@ interface CrudRouterConfig<TTable extends PgTable = PgTable, TContext = unknown,
582
599
  authorize?: (ctx: TContext, resource: TSelect, operation: CrudOperation) => boolean | Promise<boolean>;
583
600
  /**
584
601
  * 要从 schema 排除的字段(自动派生时使用)
585
- * @default ["id", "createdAt", "updatedAt"]
602
+ * @default ["id", "createdAt", "updatedAt", "createdBy", "createdByType", "updatedBy", "updatedByType"]
586
603
  */
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>[];
@@ -899,4 +945,4 @@ declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
899
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
@@ -533,7 +533,11 @@ const importInputSchema = z.object({
533
533
  const DEFAULT_OMIT_FIELDS = [
534
534
  "id",
535
535
  "createdAt",
536
- "updatedAt"
536
+ "updatedAt",
537
+ "createdBy",
538
+ "createdByType",
539
+ "updatedBy",
540
+ "updatedByType"
537
541
  ];
538
542
  function shouldEnableCrudExtensionSchema(config) {
539
543
  return Boolean(config.id) && config.extensions !== false;
@@ -547,19 +551,30 @@ function withCrudExtensionInput(schema, config) {
547
551
  * - 优先使用显式传入的 Schema
548
552
  * - 否则从 Drizzle table 自动派生
549
553
  */
550
- function resolveSchemas(config) {
551
- const { table, omitFields = DEFAULT_OMIT_FIELDS } = config;
554
+ function resolveSchemas(config, idField = "id") {
555
+ const { table } = config;
556
+ const omitFields = [...new Set([...DEFAULT_OMIT_FIELDS, ...config.omitFields ?? []])];
552
557
  let schema;
558
+ let upsertSchema;
559
+ let importSchema;
553
560
  if (config.schema) {
554
561
  if (!isZodObject(config.schema)) throw new Error("[createCrudRouter] schema must be a ZodObject (not ZodEffects or other types). If you're using .refine() or .transform(), please remove them from schema.");
555
562
  schema = config.schema;
563
+ upsertSchema = schema;
564
+ importSchema = schema;
556
565
  } else {
557
566
  const rawSchema = createInsertSchema(table);
558
567
  if (!isZodObject(rawSchema)) throw new Error("[createCrudRouter] drizzle-zod returned non-ZodObject schema. This may be due to table refinements. Please provide schema explicitly.");
559
- const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
568
+ const omitConfig = Object.fromEntries(omitFields.filter((field) => field in rawSchema.shape).map((field) => [field, true]));
560
569
  schema = rawSchema.omit(omitConfig);
570
+ const idSchema = rawSchema.shape[idField];
571
+ const schemaWithOptionalId = idSchema && !(idField in schema.shape) ? schema.extend({ [idField]: idSchema.optional() }) : schema;
572
+ upsertSchema = schemaWithOptionalId;
573
+ importSchema = schemaWithOptionalId;
561
574
  }
562
575
  schema = withCrudExtensionInput(schema, config);
576
+ upsertSchema = withCrudExtensionInput(upsertSchema, config);
577
+ importSchema = withCrudExtensionInput(importSchema, config);
563
578
  let selectSchema;
564
579
  if (config.selectSchema) selectSchema = config.selectSchema;
565
580
  else if (config.schema) selectSchema = schema;
@@ -568,6 +583,8 @@ function resolveSchemas(config) {
568
583
  return {
569
584
  selectSchema,
570
585
  schema,
586
+ upsertSchema,
587
+ importSchema,
571
588
  updateSchema
572
589
  };
573
590
  }
@@ -575,12 +592,87 @@ function getColumnRefId(columnRef) {
575
592
  return typeof columnRef === "string" ? columnRef : columnRef.id;
576
593
  }
577
594
  function findColumnRef(columnId, allowedColumns) {
595
+ if (!Array.isArray(allowedColumns)) return void 0;
578
596
  return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
579
597
  }
580
598
  function validateColumn(columnId, allowedColumns) {
581
- if (!allowedColumns || allowedColumns.length === 0) return true;
599
+ if (allowedColumns === false) return false;
600
+ if (allowedColumns === void 0) return true;
582
601
  return findColumnRef(columnId, allowedColumns) !== void 0;
583
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
+ }
584
676
  function getTableColumn(table, columnId) {
585
677
  return table[columnId];
586
678
  }
@@ -599,7 +691,8 @@ function buildJsonFieldTextExpression(column, jsonField) {
599
691
  }
600
692
  function resolveColumnTarget({ table, columnId, allowedColumns }) {
601
693
  const columnRef = findColumnRef(columnId, allowedColumns);
602
- if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
694
+ if (allowedColumns === false) return;
695
+ if (Array.isArray(allowedColumns) && columnRef === void 0) return;
603
696
  if (columnRef && typeof columnRef !== "string") {
604
697
  if (columnRef.expression) return columnRef.expression({ table });
605
698
  const column = getTableColumn(table, columnRef.id);
@@ -795,7 +888,8 @@ async function enrichCrudRows(ctx, config, idField, rows) {
795
888
  });
796
889
  }
797
890
  function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
798
- if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
891
+ if (allowedColumns === false) return false;
892
+ if (Array.isArray(allowedColumns)) return resolveColumnTarget({
799
893
  table,
800
894
  columnId,
801
895
  allowedColumns
@@ -810,11 +904,16 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
810
904
  const filters = input.filters ?? [];
811
905
  const search = typeof input.search === "string" ? input.search.trim() : "";
812
906
  if (!target || filters.length === 0 && !search) return input;
907
+ const filtersDisabled = filterableColumns === false;
908
+ const searchDisabled = searchColumns === false;
813
909
  const baseFilters = [];
814
910
  const extensionFilters = [];
815
- for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
816
- else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
817
- 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
+ }
818
917
  if (extensionFilters.length === 0 && !search) return {
819
918
  ...input,
820
919
  filters: baseFilters
@@ -834,14 +933,14 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
834
933
  });
835
934
  matchedSets.push(new Set(matchedIds$1));
836
935
  }
837
- if (search && provider?.searchEntityIds) {
936
+ if (search && !searchDisabled && provider?.searchEntityIds) {
838
937
  const matchedIds$1 = await provider.searchEntityIds({
839
938
  id: target.id,
840
939
  search,
841
940
  limit: 5e3
842
941
  });
843
942
  matchedSets.push(new Set(matchedIds$1));
844
- } else if (search && !buildCrudSearchCondition({
943
+ } else if (search && !searchDisabled && !buildCrudSearchCondition({
845
944
  table,
846
945
  search,
847
946
  searchColumns
@@ -868,7 +967,7 @@ async function applyCrudExtensionFilters(ctx, config, table, idField, filterable
868
967
  }
869
968
  function buildCrudSearchCondition({ table, search, searchColumns }) {
870
969
  const term = typeof search === "string" ? search.trim() : "";
871
- if (!term || !searchColumns || searchColumns.length === 0) return void 0;
970
+ if (!term || !Array.isArray(searchColumns) || searchColumns.length === 0) return;
872
971
  const conditions = searchColumns.map((columnRef) => resolveColumnTarget({
873
972
  table,
874
973
  columnId: getColumnRefId(columnRef),
@@ -897,8 +996,17 @@ function combineConditions(...conditions) {
897
996
  * @typeParam TUpdate - 更新输入类型
898
997
  */
899
998
  function createCrudRouter(config) {
900
- const { table, idField = "id", filterableColumns, searchColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
901
- const { selectSchema, schema, updateSchema } = resolveSchemas(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
+ };
1009
+ const { selectSchema, schema, upsertSchema, importSchema, updateSchema } = resolveSchemas(config, idField);
902
1010
  const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
903
1011
  const listOutputSchema = z.object({
904
1012
  data: z.array(entityOutputSchema),
@@ -910,7 +1018,21 @@ function createCrudRouter(config) {
910
1018
  const metadataOutputSchema = z.object({
911
1019
  schema: z.unknown().optional(),
912
1020
  fields: z.record(z.string(), z.unknown()).optional(),
913
- 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
+ })
914
1036
  });
915
1037
  const deleteManyOutputSchema = z.object({ deleted: z.number() });
916
1038
  const updateManyOutputSchema = z.object({ updated: z.number() });
@@ -987,10 +1109,10 @@ function createCrudRouter(config) {
987
1109
  meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
988
1110
  await withGuard(ctx, "list");
989
1111
  const target = readCrudTarget(config);
990
- if (!target) return {};
1112
+ if (!target) return withCrudCapabilities({}, capabilities, disabledCapabilities);
991
1113
  const provider = resolveCrudExtensions(ctx, config);
992
- if (!provider?.getMetadata) return {};
993
- 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);
994
1116
  }),
995
1117
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
996
1118
  await withGuard(ctx, "list");
@@ -1018,9 +1140,12 @@ function createCrudRouter(config) {
1018
1140
  })));
1019
1141
  let query = ctx.db.select().from(table).$dynamic();
1020
1142
  if (where) query = query.where(where);
1021
- if (effectiveInput.sort?.length) {
1022
- const sortField = effectiveInput.sort[0];
1023
- if (sortField && validateColumn(sortField.id, sortableColumns)) {
1143
+ const sortField = effectiveInput.sort?.[0] ?? (getTableColumn(table, "createdAt") ? {
1144
+ id: "createdAt",
1145
+ desc: true
1146
+ } : void 0);
1147
+ if (sortField) {
1148
+ if (validateColumn(sortField.id, sortableColumns)) {
1024
1149
  const column = resolveColumnTarget({
1025
1150
  table,
1026
1151
  columnId: sortField.id,
@@ -1226,7 +1351,7 @@ function createCrudRouter(config) {
1226
1351
  });
1227
1352
  return doUpdateMany(input.data);
1228
1353
  }),
1229
- upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
1354
+ upsert: resolved.procedureFactory("upsert").input(upsertSchema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
1230
1355
  await withGuard(ctx, "upsert");
1231
1356
  const doUpsert = async (inputData) => {
1232
1357
  const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
@@ -1371,7 +1496,7 @@ function createCrudRouter(config) {
1371
1496
  const failed = [];
1372
1497
  for (let i = 0; i < importData$1.rows.length; i++) {
1373
1498
  const row = importData$1.rows[i];
1374
- const result = schema.safeParse(row);
1499
+ const result = importSchema.safeParse(row);
1375
1500
  if (result.success) {
1376
1501
  const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1377
1502
  validRows.push(splitRow);
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.6",
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",
@@ -48,10 +48,10 @@
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^4.0.18",
50
50
  "zod": "^4.3.6",
51
+ "@internal/eslint-config": "0.3.0",
52
+ "@internal/prettier-config": "0.0.1",
51
53
  "@internal/tsconfig": "0.1.0",
52
54
  "@internal/tsdown-config": "0.1.0",
53
- "@internal/prettier-config": "0.0.1",
54
- "@internal/eslint-config": "0.3.0",
55
55
  "@internal/vitest-config": "0.1.0"
56
56
  },
57
57
  "prettier": "@internal/prettier-config",