@wordrhyme/auto-crud-server 1.0.4 → 1.0.6
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 +373 -175
- package/dist/index.cjs +116 -44
- package/dist/index.d.cts +207 -95
- package/dist/index.d.ts +206 -94
- package/dist/index.js +114 -45
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,8 +15,15 @@ const publicProcedure = t.procedure;
|
|
|
15
15
|
/**
|
|
16
16
|
* 检查列是否为空
|
|
17
17
|
*/
|
|
18
|
+
function toSqlTarget(column) {
|
|
19
|
+
return sql`${column}`;
|
|
20
|
+
}
|
|
21
|
+
function isBooleanColumn(column) {
|
|
22
|
+
return "dataType" in column && column.dataType === "boolean";
|
|
23
|
+
}
|
|
18
24
|
function isEmpty(column) {
|
|
19
|
-
|
|
25
|
+
const expr = toSqlTarget(column);
|
|
26
|
+
return or(isNull(expr), eq(expr, ""));
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* 安全解析日期时间戳
|
|
@@ -39,22 +46,24 @@ function safeParseNumber(value) {
|
|
|
39
46
|
const num = Number(value);
|
|
40
47
|
return Number.isNaN(num) ? null : num;
|
|
41
48
|
}
|
|
42
|
-
function filterColumns({ table, filters, joinOperator }) {
|
|
49
|
+
function filterColumns({ table, filters, joinOperator, resolveColumn }) {
|
|
43
50
|
const joinFn = joinOperator === "and" ? and : or;
|
|
44
51
|
const validConditions = filters.map((filter) => {
|
|
45
|
-
const column = getColumn(table, filter.id);
|
|
52
|
+
const column = resolveColumn ? resolveColumn(filter.id) : getColumn(table, filter.id);
|
|
53
|
+
if (!column) return void 0;
|
|
54
|
+
const expr = toSqlTarget(column);
|
|
46
55
|
switch (filter.operator) {
|
|
47
|
-
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? ilike(
|
|
48
|
-
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(
|
|
56
|
+
case "iLike": return filter.variant === "text" && typeof filter.value === "string" ? ilike(expr, `%${filter.value}%`) : void 0;
|
|
57
|
+
case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(expr, `%${filter.value}%`) : void 0;
|
|
49
58
|
case "eq":
|
|
50
|
-
if (column
|
|
59
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return eq(expr, filter.value === "true");
|
|
51
60
|
if (filter.variant === "dateRange" && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
52
61
|
const startDate = safeParseDate(filter.value[0]);
|
|
53
62
|
const endDate = safeParseDate(filter.value[1]);
|
|
54
63
|
if (!startDate && !endDate) return void 0;
|
|
55
64
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
56
65
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
57
|
-
return and(startDate ? gte(
|
|
66
|
+
return and(startDate ? gte(expr, startDate) : void 0, endDate ? lte(expr, endDate) : void 0);
|
|
58
67
|
}
|
|
59
68
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
60
69
|
const date = safeParseDate(filter.value);
|
|
@@ -62,72 +71,72 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
62
71
|
date.setHours(0, 0, 0, 0);
|
|
63
72
|
const end = new Date(date);
|
|
64
73
|
end.setHours(23, 59, 59, 999);
|
|
65
|
-
return and(gte(
|
|
74
|
+
return and(gte(expr, date), lte(expr, end));
|
|
66
75
|
}
|
|
67
|
-
return eq(
|
|
76
|
+
return eq(expr, filter.value);
|
|
68
77
|
case "ne":
|
|
69
|
-
if (column
|
|
78
|
+
if (isBooleanColumn(column) && typeof filter.value === "string") return ne(expr, filter.value === "true");
|
|
70
79
|
if (filter.variant === "date" || filter.variant === "dateRange") {
|
|
71
80
|
const date = safeParseDate(filter.value);
|
|
72
81
|
if (!date) return void 0;
|
|
73
82
|
date.setHours(0, 0, 0, 0);
|
|
74
83
|
const end = new Date(date);
|
|
75
84
|
end.setHours(23, 59, 59, 999);
|
|
76
|
-
return or(lt(
|
|
85
|
+
return or(lt(expr, date), gt(expr, end));
|
|
77
86
|
}
|
|
78
|
-
return ne(
|
|
87
|
+
return ne(expr, filter.value);
|
|
79
88
|
case "inArray":
|
|
80
|
-
if (Array.isArray(filter.value)) return inArray(
|
|
89
|
+
if (Array.isArray(filter.value)) return inArray(expr, filter.value);
|
|
81
90
|
return;
|
|
82
91
|
case "notInArray":
|
|
83
|
-
if (Array.isArray(filter.value)) return notInArray(
|
|
92
|
+
if (Array.isArray(filter.value)) return notInArray(expr, filter.value);
|
|
84
93
|
return;
|
|
85
94
|
case "lt":
|
|
86
95
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
87
96
|
const num = safeParseNumber(filter.value);
|
|
88
|
-
return num !== null ? lt(
|
|
97
|
+
return num !== null ? lt(expr, num) : void 0;
|
|
89
98
|
}
|
|
90
99
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
91
100
|
const date = safeParseDate(filter.value);
|
|
92
101
|
if (!date) return void 0;
|
|
93
102
|
date.setHours(23, 59, 59, 999);
|
|
94
|
-
return lt(
|
|
103
|
+
return lt(expr, date);
|
|
95
104
|
}
|
|
96
105
|
return;
|
|
97
106
|
case "lte":
|
|
98
107
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
99
108
|
const num = safeParseNumber(filter.value);
|
|
100
|
-
return num !== null ? lte(
|
|
109
|
+
return num !== null ? lte(expr, num) : void 0;
|
|
101
110
|
}
|
|
102
111
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
103
112
|
const date = safeParseDate(filter.value);
|
|
104
113
|
if (!date) return void 0;
|
|
105
114
|
date.setHours(23, 59, 59, 999);
|
|
106
|
-
return lte(
|
|
115
|
+
return lte(expr, date);
|
|
107
116
|
}
|
|
108
117
|
return;
|
|
109
118
|
case "gt":
|
|
110
119
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
111
120
|
const num = safeParseNumber(filter.value);
|
|
112
|
-
return num !== null ? gt(
|
|
121
|
+
return num !== null ? gt(expr, num) : void 0;
|
|
113
122
|
}
|
|
114
123
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
115
124
|
const date = safeParseDate(filter.value);
|
|
116
125
|
if (!date) return void 0;
|
|
117
126
|
date.setHours(0, 0, 0, 0);
|
|
118
|
-
return gt(
|
|
127
|
+
return gt(expr, date);
|
|
119
128
|
}
|
|
120
129
|
return;
|
|
121
130
|
case "gte":
|
|
122
131
|
if (filter.variant === "number" || filter.variant === "range") {
|
|
123
132
|
const num = safeParseNumber(filter.value);
|
|
124
|
-
return num !== null ? gte(
|
|
133
|
+
return num !== null ? gte(expr, num) : void 0;
|
|
125
134
|
}
|
|
126
135
|
if (filter.variant === "date" && typeof filter.value === "string") {
|
|
127
136
|
const date = safeParseDate(filter.value);
|
|
128
137
|
if (!date) return void 0;
|
|
129
138
|
date.setHours(0, 0, 0, 0);
|
|
130
|
-
return gte(
|
|
139
|
+
return gte(expr, date);
|
|
131
140
|
}
|
|
132
141
|
return;
|
|
133
142
|
case "isBetween":
|
|
@@ -137,15 +146,15 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
137
146
|
if (!startDate && !endDate) return void 0;
|
|
138
147
|
if (startDate) startDate.setHours(0, 0, 0, 0);
|
|
139
148
|
if (endDate) endDate.setHours(23, 59, 59, 999);
|
|
140
|
-
return and(startDate ? gte(
|
|
149
|
+
return and(startDate ? gte(expr, startDate) : void 0, endDate ? lte(expr, endDate) : void 0);
|
|
141
150
|
}
|
|
142
151
|
if ((filter.variant === "number" || filter.variant === "range") && Array.isArray(filter.value) && filter.value.length === 2) {
|
|
143
152
|
const firstValue = safeParseNumber(filter.value[0]);
|
|
144
153
|
const secondValue = safeParseNumber(filter.value[1]);
|
|
145
154
|
if (firstValue === null && secondValue === null) return;
|
|
146
|
-
if (firstValue !== null && secondValue === null) return eq(
|
|
147
|
-
if (firstValue === null && secondValue !== null) return eq(
|
|
148
|
-
return and(firstValue !== null ? gte(
|
|
155
|
+
if (firstValue !== null && secondValue === null) return eq(expr, firstValue);
|
|
156
|
+
if (firstValue === null && secondValue !== null) return eq(expr, secondValue);
|
|
157
|
+
return and(firstValue !== null ? gte(expr, firstValue) : void 0, secondValue !== null ? lte(expr, secondValue) : void 0);
|
|
149
158
|
}
|
|
150
159
|
return;
|
|
151
160
|
case "isRelativeToToday":
|
|
@@ -170,11 +179,11 @@ function filterColumns({ table, filters, joinOperator }) {
|
|
|
170
179
|
break;
|
|
171
180
|
default: return;
|
|
172
181
|
}
|
|
173
|
-
return and(gte(
|
|
182
|
+
return and(gte(expr, startDate), lte(expr, endDate));
|
|
174
183
|
}
|
|
175
184
|
return;
|
|
176
|
-
case "isEmpty": return isEmpty(
|
|
177
|
-
case "isNotEmpty": return not(isEmpty(
|
|
185
|
+
case "isEmpty": return isEmpty(expr);
|
|
186
|
+
case "isNotEmpty": return not(isEmpty(expr));
|
|
178
187
|
default: throw new Error(`Unsupported operator: ${filter.operator}`);
|
|
179
188
|
}
|
|
180
189
|
}).filter((condition) => condition !== void 0);
|
|
@@ -471,7 +480,7 @@ const filterItemSchema = z.object({
|
|
|
471
480
|
operator: z.enum(dataTableConfig.operators),
|
|
472
481
|
filterId: z.string()
|
|
473
482
|
});
|
|
474
|
-
const
|
|
483
|
+
const baseListInputSchema = z.object({
|
|
475
484
|
page: z.number().min(1).default(1),
|
|
476
485
|
perPage: z.number().min(1).max(100).default(10),
|
|
477
486
|
sort: z.array(z.object({
|
|
@@ -481,7 +490,9 @@ const listInputSchema = z.object({
|
|
|
481
490
|
filters: z.array(filterItemSchema).optional(),
|
|
482
491
|
joinOperator: z.enum(["and", "or"]).default("and")
|
|
483
492
|
});
|
|
484
|
-
const
|
|
493
|
+
const baseGetInputSchema = z.object({ id: z.string() });
|
|
494
|
+
const defaultGetInputSchema = z.union([z.string(), baseGetInputSchema]);
|
|
495
|
+
const baseExportInputSchema = z.object({
|
|
485
496
|
sort: z.array(z.object({
|
|
486
497
|
id: z.string(),
|
|
487
498
|
desc: z.boolean()
|
|
@@ -531,9 +542,43 @@ function resolveSchemas(config) {
|
|
|
531
542
|
updateSchema
|
|
532
543
|
};
|
|
533
544
|
}
|
|
545
|
+
function getColumnRefId(columnRef) {
|
|
546
|
+
return typeof columnRef === "string" ? columnRef : columnRef.id;
|
|
547
|
+
}
|
|
548
|
+
function findColumnRef(columnId, allowedColumns) {
|
|
549
|
+
return allowedColumns?.find((columnRef) => getColumnRefId(columnRef) === columnId);
|
|
550
|
+
}
|
|
534
551
|
function validateColumn(columnId, allowedColumns) {
|
|
535
552
|
if (!allowedColumns || allowedColumns.length === 0) return true;
|
|
536
|
-
return
|
|
553
|
+
return findColumnRef(columnId, allowedColumns) !== void 0;
|
|
554
|
+
}
|
|
555
|
+
function getTableColumn(table, columnId) {
|
|
556
|
+
return table[columnId];
|
|
557
|
+
}
|
|
558
|
+
function normalizeJsonFields(jsonField) {
|
|
559
|
+
return (Array.isArray(jsonField) ? jsonField : [jsonField]).filter((field) => field.length > 0);
|
|
560
|
+
}
|
|
561
|
+
function buildJsonFieldTextExpression(column, jsonField) {
|
|
562
|
+
const fields = normalizeJsonFields(jsonField);
|
|
563
|
+
if (fields.length === 0) throw new Error("[createCrudRouter] jsonField must include at least one json key.");
|
|
564
|
+
const objectFields = fields.map((field) => sql`${column} ->> ${field}`);
|
|
565
|
+
return sql`case jsonb_typeof(${column})
|
|
566
|
+
when 'object' then coalesce(${sql.join(objectFields, sql`, `)}, '')
|
|
567
|
+
when 'string' then ${column} #>> '{}'
|
|
568
|
+
else ''
|
|
569
|
+
end`;
|
|
570
|
+
}
|
|
571
|
+
function resolveColumnTarget({ table, columnId, allowedColumns }) {
|
|
572
|
+
const columnRef = findColumnRef(columnId, allowedColumns);
|
|
573
|
+
if ((allowedColumns?.length ?? 0) > 0 && columnRef === void 0) return;
|
|
574
|
+
if (columnRef && typeof columnRef !== "string") {
|
|
575
|
+
if (columnRef.expression) return columnRef.expression({ table });
|
|
576
|
+
const column = getTableColumn(table, columnRef.id);
|
|
577
|
+
if (!column) return void 0;
|
|
578
|
+
if (columnRef.jsonField) return buildJsonFieldTextExpression(column, columnRef.jsonField);
|
|
579
|
+
return column;
|
|
580
|
+
}
|
|
581
|
+
return getTableColumn(table, columnId);
|
|
537
582
|
}
|
|
538
583
|
/**
|
|
539
584
|
* 创建 CRUD Router
|
|
@@ -584,6 +629,9 @@ function createCrudRouter(config) {
|
|
|
584
629
|
});
|
|
585
630
|
const resolved = resolveConfig(config);
|
|
586
631
|
const softDelete = resolveSoftDelete(softDeleteOption);
|
|
632
|
+
const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
|
|
633
|
+
const resolvedGetInputSchema = config.getInputSchema ? z.union([z.string(), config.getInputSchema]) : defaultGetInputSchema;
|
|
634
|
+
const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
|
|
587
635
|
const m = middleware;
|
|
588
636
|
const getIdColumn = () => table[idField];
|
|
589
637
|
const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
|
|
@@ -625,7 +673,7 @@ function createCrudRouter(config) {
|
|
|
625
673
|
});
|
|
626
674
|
};
|
|
627
675
|
const procedures = {
|
|
628
|
-
list: resolved.procedureFactory("list").input(
|
|
676
|
+
list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
|
|
629
677
|
await withGuard(ctx, "list");
|
|
630
678
|
const doList = async (listInput$1) => {
|
|
631
679
|
const offset = (listInput$1.page - 1) * listInput$1.perPage;
|
|
@@ -633,14 +681,23 @@ function createCrudRouter(config) {
|
|
|
633
681
|
const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
|
|
634
682
|
table,
|
|
635
683
|
filters: validatedFilters,
|
|
636
|
-
joinOperator: listInput$1.joinOperator
|
|
684
|
+
joinOperator: listInput$1.joinOperator,
|
|
685
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
686
|
+
table,
|
|
687
|
+
columnId: String(columnId),
|
|
688
|
+
allowedColumns: filterableColumns
|
|
689
|
+
})
|
|
637
690
|
}) : void 0);
|
|
638
691
|
let query = ctx.db.select().from(table).$dynamic();
|
|
639
692
|
if (where) query = query.where(where);
|
|
640
693
|
if (listInput$1.sort?.length) {
|
|
641
694
|
const sortField = listInput$1.sort[0];
|
|
642
695
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
643
|
-
const column =
|
|
696
|
+
const column = resolveColumnTarget({
|
|
697
|
+
table,
|
|
698
|
+
columnId: sortField.id,
|
|
699
|
+
allowedColumns: sortableColumns
|
|
700
|
+
});
|
|
644
701
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
645
702
|
}
|
|
646
703
|
}
|
|
@@ -664,20 +721,23 @@ function createCrudRouter(config) {
|
|
|
664
721
|
});
|
|
665
722
|
return doList(listInput);
|
|
666
723
|
}),
|
|
667
|
-
get: resolved.procedureFactory("get").input(
|
|
724
|
+
get: resolved.procedureFactory("get").input(resolvedGetInputSchema).output(entityOutputSchema.nullable()).query(async ({ ctx, input }) => {
|
|
668
725
|
await withGuard(ctx, "get");
|
|
669
|
-
const doGet = async () => {
|
|
670
|
-
const
|
|
726
|
+
const doGet = async (getInput) => {
|
|
727
|
+
const id$1 = typeof getInput === "string" ? getInput : getInput.id;
|
|
728
|
+
const where = buildWhere(ctx, "get", eq(getIdColumn(), id$1));
|
|
671
729
|
const [item] = await ctx.db.select().from(table).where(where);
|
|
672
730
|
await withAuthorize(ctx, item, "get");
|
|
673
731
|
return item ?? null;
|
|
674
732
|
};
|
|
733
|
+
const id = typeof input === "string" ? input : input.id;
|
|
675
734
|
if (m.get) return m.get({
|
|
676
735
|
ctx,
|
|
677
|
-
id
|
|
678
|
-
|
|
736
|
+
id,
|
|
737
|
+
input,
|
|
738
|
+
next: async (modifiedInput) => doGet(modifiedInput ?? input)
|
|
679
739
|
});
|
|
680
|
-
return doGet();
|
|
740
|
+
return doGet(input);
|
|
681
741
|
}),
|
|
682
742
|
create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
|
|
683
743
|
await withGuard(ctx, "create");
|
|
@@ -823,7 +883,7 @@ function createCrudRouter(config) {
|
|
|
823
883
|
});
|
|
824
884
|
return doUpsert(input);
|
|
825
885
|
}),
|
|
826
|
-
export: resolved.procedureFactory("export").input(
|
|
886
|
+
export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
|
|
827
887
|
await withGuard(ctx, "export");
|
|
828
888
|
const doExport = async (exportInput$1) => {
|
|
829
889
|
const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
|
|
@@ -831,14 +891,23 @@ function createCrudRouter(config) {
|
|
|
831
891
|
const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
|
|
832
892
|
table,
|
|
833
893
|
filters: validatedFilters,
|
|
834
|
-
joinOperator: exportInput$1.joinOperator ?? "and"
|
|
894
|
+
joinOperator: exportInput$1.joinOperator ?? "and",
|
|
895
|
+
resolveColumn: (columnId) => resolveColumnTarget({
|
|
896
|
+
table,
|
|
897
|
+
columnId: String(columnId),
|
|
898
|
+
allowedColumns: filterableColumns
|
|
899
|
+
})
|
|
835
900
|
}) : void 0);
|
|
836
901
|
let query = ctx.db.select().from(table).$dynamic();
|
|
837
902
|
if (where) query = query.where(where);
|
|
838
903
|
if (exportInput$1.sort?.length) {
|
|
839
904
|
const sortField = exportInput$1.sort[0];
|
|
840
905
|
if (sortField && validateColumn(sortField.id, sortableColumns)) {
|
|
841
|
-
const column =
|
|
906
|
+
const column = resolveColumnTarget({
|
|
907
|
+
table,
|
|
908
|
+
columnId: sortField.id,
|
|
909
|
+
allowedColumns: sortableColumns
|
|
910
|
+
});
|
|
842
911
|
if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
|
|
843
912
|
}
|
|
844
913
|
}
|
|
@@ -1026,4 +1095,4 @@ function composeMiddleware(...middlewares) {
|
|
|
1026
1095
|
const appRouter = router({});
|
|
1027
1096
|
|
|
1028
1097
|
//#endregion
|
|
1029
|
-
export { afterMiddleware, appRouter, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
|
1098
|
+
export { afterMiddleware, appRouter, baseExportInputSchema, baseGetInputSchema, baseListInputSchema, beforeMiddleware, composeMiddleware, createCrudRouter, publicProcedure, router };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordrhyme/auto-crud-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.6",
|
|
5
5
|
"description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
|
|
6
6
|
"author": "wordrhyme",
|
|
7
7
|
"license": "MIT",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"vitest": "^4.0.18",
|
|
49
49
|
"zod": "^4.1.13",
|
|
50
50
|
"@internal/eslint-config": "0.3.0",
|
|
51
|
+
"@internal/prettier-config": "0.0.1",
|
|
51
52
|
"@internal/tsconfig": "0.1.0",
|
|
52
53
|
"@internal/tsdown-config": "0.1.0",
|
|
53
|
-
"@internal/prettier-config": "0.0.1",
|
|
54
54
|
"@internal/vitest-config": "0.1.0"
|
|
55
55
|
},
|
|
56
56
|
"prettier": "@internal/prettier-config",
|