@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/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
- return or(isNull(column), eq(column, ""));
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(column, `%${filter.value}%`) : void 0;
48
- case "notILike": return filter.variant === "text" && typeof filter.value === "string" ? notIlike(column, `%${filter.value}%`) : void 0;
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.dataType === "boolean" && typeof filter.value === "string") return eq(column, filter.value === "true");
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(column, startDate) : void 0, endDate ? lte(column, endDate) : void 0);
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(column, date), lte(column, end));
74
+ return and(gte(expr, date), lte(expr, end));
66
75
  }
67
- return eq(column, filter.value);
76
+ return eq(expr, filter.value);
68
77
  case "ne":
69
- if (column.dataType === "boolean" && typeof filter.value === "string") return ne(column, filter.value === "true");
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(column, date), gt(column, end));
85
+ return or(lt(expr, date), gt(expr, end));
77
86
  }
78
- return ne(column, filter.value);
87
+ return ne(expr, filter.value);
79
88
  case "inArray":
80
- if (Array.isArray(filter.value)) return inArray(column, filter.value);
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(column, filter.value);
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(column, num) : void 0;
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(column, date);
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(column, num) : void 0;
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(column, date);
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(column, num) : void 0;
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(column, date);
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(column, num) : void 0;
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(column, date);
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(column, startDate) : void 0, endDate ? lte(column, endDate) : void 0);
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(column, firstValue);
147
- if (firstValue === null && secondValue !== null) return eq(column, secondValue);
148
- return and(firstValue !== null ? gte(column, firstValue) : void 0, secondValue !== null ? lte(column, secondValue) : void 0);
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(column, startDate), lte(column, endDate));
182
+ return and(gte(expr, startDate), lte(expr, endDate));
174
183
  }
175
184
  return;
176
- case "isEmpty": return isEmpty(column);
177
- case "isNotEmpty": return not(isEmpty(column));
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 listInputSchema = z.object({
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 exportInputSchema = z.object({
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 allowedColumns.includes(columnId);
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(listInputSchema).output(listOutputSchema).query(async ({ ctx, 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 = table[sortField.id];
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(z.string()).output(entityOutputSchema.nullable()).query(async ({ ctx, 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 where = buildWhere(ctx, "get", eq(getIdColumn(), input));
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: input,
678
- next: doGet
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(exportInputSchema).output(exportOutputSchema).query(async ({ ctx, 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 = table[sortField.id];
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",
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",