@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 +14 -11
- package/dist/index.cjs +149 -24
- package/dist/index.d.cts +55 -9
- package/dist/index.d.ts +48 -2
- package/dist/index.js +149 -24
- package/package.json +3 -3
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
|
|
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
|
-
//
|
|
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`
|
|
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: ['
|
|
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: ['
|
|
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
|
|
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((
|
|
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 (
|
|
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 (
|
|
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
|
|
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)
|
|
845
|
-
|
|
846
|
-
|
|
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
|
|
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",
|
|
930
|
-
const
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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:
|
|
15
|
+
declare const router: _trpc_server0.TRPCRouterBuilder<{
|
|
16
16
|
ctx: Context;
|
|
17
17
|
meta: object;
|
|
18
|
-
errorShape:
|
|
18
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
19
19
|
transformer: true;
|
|
20
20
|
}>;
|
|
21
|
-
declare const publicProcedure:
|
|
21
|
+
declare const publicProcedure: _trpc_server0.TRPCProcedureBuilder<Context, object, object, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, _trpc_server0.TRPCUnsetMarker, false>;
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/types/config.d.ts
|
|
24
24
|
type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete' | 'deleteMany' | 'updateMany' | 'upsert' | 'export' | 'import' | 'createMany';
|
|
@@ -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:
|
|
939
|
+
declare const appRouter: _trpc_server0.TRPCBuiltRouter<{
|
|
894
940
|
ctx: Context;
|
|
895
941
|
meta: object;
|
|
896
|
-
errorShape:
|
|
942
|
+
errorShape: _trpc_server0.TRPCDefaultErrorShape;
|
|
897
943
|
transformer: true;
|
|
898
|
-
},
|
|
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
|
|
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((
|
|
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 (
|
|
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 (
|
|
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
|
|
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)
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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",
|
|
901
|
-
const
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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(
|
|
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 =
|
|
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.
|
|
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",
|