@wordrhyme/auto-crud-server 1.0.4 → 1.0.5
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 +319 -175
- package/dist/index.cjs +21 -10
- package/dist/index.d.cts +162 -90
- package/dist/index.d.ts +161 -89
- package/dist/index.js +19 -11
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -52,20 +52,24 @@ yarn add @wordrhyme/auto-crud-server
|
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
54
|
// src/db/schema.ts
|
|
55
|
-
import { pgTable, varchar, real, boolean, timestamp } from
|
|
55
|
+
import { pgTable, varchar, real, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
56
56
|
|
|
57
|
-
export const tasks = pgTable(
|
|
58
|
-
id: varchar(
|
|
59
|
-
title: varchar(
|
|
60
|
-
status: varchar(
|
|
61
|
-
enum: [
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
export const tasks = pgTable('tasks', {
|
|
58
|
+
id: varchar('id', { length: 30 }).primaryKey(),
|
|
59
|
+
title: varchar('title', { length: 128 }).notNull(),
|
|
60
|
+
status: varchar('status', {
|
|
61
|
+
enum: ['todo', 'in-progress', 'done', 'canceled'],
|
|
62
|
+
})
|
|
63
|
+
.notNull()
|
|
64
|
+
.default('todo'),
|
|
65
|
+
priority: varchar('priority', {
|
|
66
|
+
enum: ['low', 'medium', 'high'],
|
|
67
|
+
})
|
|
68
|
+
.notNull()
|
|
69
|
+
.default('low'),
|
|
70
|
+
estimatedHours: real('estimated_hours').default(0),
|
|
71
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
72
|
+
updatedAt: timestamp('updated_at').defaultNow(),
|
|
69
73
|
});
|
|
70
74
|
```
|
|
71
75
|
|
|
@@ -73,8 +77,8 @@ export const tasks = pgTable("tasks", {
|
|
|
73
77
|
|
|
74
78
|
```typescript
|
|
75
79
|
// src/server/routers/tasks.ts
|
|
76
|
-
import { createCrudRouter } from
|
|
77
|
-
import { tasks } from
|
|
80
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
81
|
+
import { tasks } from '@/db/schema';
|
|
78
82
|
|
|
79
83
|
// 🚀 零配置!一行代码生成完整 CRUD 路由
|
|
80
84
|
export const tasksRouter = createCrudRouter({
|
|
@@ -86,9 +90,9 @@ export const tasksRouter = createCrudRouter({
|
|
|
86
90
|
**或者,显式传入 Schema:**
|
|
87
91
|
|
|
88
92
|
```typescript
|
|
89
|
-
import { createCrudRouter } from
|
|
90
|
-
import { tasks } from
|
|
91
|
-
import { createSelectSchema } from
|
|
93
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
94
|
+
import { tasks } from '@/db/schema';
|
|
95
|
+
import { createSelectSchema } from 'drizzle-zod';
|
|
92
96
|
|
|
93
97
|
// 从 Drizzle Schema 自动生成 Zod Schema
|
|
94
98
|
const taskSchema = createSelectSchema(tasks).omit({
|
|
@@ -99,7 +103,7 @@ const taskSchema = createSelectSchema(tasks).omit({
|
|
|
99
103
|
|
|
100
104
|
export const tasksRouter = createCrudRouter({
|
|
101
105
|
table: tasks,
|
|
102
|
-
schema: taskSchema,
|
|
106
|
+
schema: taskSchema, // 主 Schema(用于 create/upsert)
|
|
103
107
|
// updateSchema 自动派生为 schema.partial()
|
|
104
108
|
});
|
|
105
109
|
```
|
|
@@ -108,8 +112,8 @@ export const tasksRouter = createCrudRouter({
|
|
|
108
112
|
|
|
109
113
|
```typescript
|
|
110
114
|
// src/server/routers/index.ts
|
|
111
|
-
import { router } from
|
|
112
|
-
import { tasksRouter } from
|
|
115
|
+
import { router } from '../trpc';
|
|
116
|
+
import { tasksRouter } from './tasks';
|
|
113
117
|
|
|
114
118
|
export const appRouter = router({
|
|
115
119
|
tasks: tasksRouter,
|
|
@@ -122,13 +126,13 @@ export type AppRouter = typeof appRouter;
|
|
|
122
126
|
|
|
123
127
|
```typescript
|
|
124
128
|
// src/app/api/trpc/[trpc]/route.ts (Next.js App Router)
|
|
125
|
-
import { fetchRequestHandler } from
|
|
126
|
-
import { appRouter } from
|
|
127
|
-
import { db } from
|
|
129
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
130
|
+
import { appRouter } from '@/server/routers';
|
|
131
|
+
import { db } from '@/db';
|
|
128
132
|
|
|
129
133
|
const handler = (req: Request) =>
|
|
130
134
|
fetchRequestHandler({
|
|
131
|
-
endpoint:
|
|
135
|
+
endpoint: '/api/trpc',
|
|
132
136
|
req,
|
|
133
137
|
router: appRouter,
|
|
134
138
|
createContext: () => ({ db }),
|
|
@@ -146,6 +150,7 @@ export { handler as GET, handler as POST };
|
|
|
146
150
|
### 1. `list` - 列表查询
|
|
147
151
|
|
|
148
152
|
**输入**:
|
|
153
|
+
|
|
149
154
|
```typescript
|
|
150
155
|
{
|
|
151
156
|
page: number; // 页码(从 1 开始)
|
|
@@ -165,6 +170,7 @@ export { handler as GET, handler as POST };
|
|
|
165
170
|
```
|
|
166
171
|
|
|
167
172
|
**输出**:
|
|
173
|
+
|
|
168
174
|
```typescript
|
|
169
175
|
{
|
|
170
176
|
data: Task[]; // 数据列表
|
|
@@ -173,117 +179,138 @@ export { handler as GET, handler as POST };
|
|
|
173
179
|
```
|
|
174
180
|
|
|
175
181
|
**示例**:
|
|
182
|
+
|
|
176
183
|
```typescript
|
|
177
184
|
const result = await trpc.tasks.list({
|
|
178
185
|
page: 1,
|
|
179
186
|
perPage: 10,
|
|
180
|
-
sort: [{ id:
|
|
187
|
+
sort: [{ id: 'createdAt', desc: true }],
|
|
181
188
|
filters: [
|
|
182
|
-
{ id:
|
|
183
|
-
{ id:
|
|
189
|
+
{ id: 'status', value: 'done', operator: 'eq', variant: 'select' },
|
|
190
|
+
{ id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
|
|
184
191
|
],
|
|
185
|
-
joinOperator:
|
|
192
|
+
joinOperator: 'and',
|
|
186
193
|
});
|
|
187
194
|
```
|
|
188
195
|
|
|
189
196
|
### 2. `get` - 单条查询
|
|
190
197
|
|
|
191
198
|
**输入**:
|
|
199
|
+
|
|
192
200
|
```typescript
|
|
193
|
-
{
|
|
201
|
+
{
|
|
202
|
+
id: string;
|
|
203
|
+
}
|
|
194
204
|
```
|
|
195
205
|
|
|
196
206
|
**输出**:
|
|
207
|
+
|
|
197
208
|
```typescript
|
|
198
|
-
Task
|
|
209
|
+
Task;
|
|
199
210
|
```
|
|
200
211
|
|
|
201
212
|
**示例**:
|
|
213
|
+
|
|
202
214
|
```typescript
|
|
203
|
-
const task = await trpc.tasks.get({ id:
|
|
215
|
+
const task = await trpc.tasks.get({ id: '123' });
|
|
204
216
|
```
|
|
205
217
|
|
|
206
218
|
### 3. `create` - 创建
|
|
207
219
|
|
|
208
220
|
**输入**:
|
|
221
|
+
|
|
209
222
|
```typescript
|
|
210
|
-
Omit<Task,
|
|
223
|
+
Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;
|
|
211
224
|
```
|
|
212
225
|
|
|
213
226
|
**输出**:
|
|
227
|
+
|
|
214
228
|
```typescript
|
|
215
|
-
Task
|
|
229
|
+
Task;
|
|
216
230
|
```
|
|
217
231
|
|
|
218
232
|
**示例**:
|
|
233
|
+
|
|
219
234
|
```typescript
|
|
220
235
|
const newTask = await trpc.tasks.create({
|
|
221
|
-
title:
|
|
222
|
-
status:
|
|
223
|
-
priority:
|
|
236
|
+
title: 'New Task',
|
|
237
|
+
status: 'todo',
|
|
238
|
+
priority: 'high',
|
|
224
239
|
});
|
|
225
240
|
```
|
|
226
241
|
|
|
227
242
|
### 4. `update` - 更新
|
|
228
243
|
|
|
229
244
|
**输入**:
|
|
245
|
+
|
|
230
246
|
```typescript
|
|
231
247
|
{
|
|
232
248
|
id: string;
|
|
233
|
-
data: Partial<Omit<Task,
|
|
249
|
+
data: Partial<Omit<Task, 'id' | 'createdAt' | 'updatedAt'>>;
|
|
234
250
|
}
|
|
235
251
|
```
|
|
236
252
|
|
|
237
253
|
**输出**:
|
|
254
|
+
|
|
238
255
|
```typescript
|
|
239
|
-
Task
|
|
256
|
+
Task;
|
|
240
257
|
```
|
|
241
258
|
|
|
242
259
|
**示例**:
|
|
260
|
+
|
|
243
261
|
```typescript
|
|
244
262
|
const updatedTask = await trpc.tasks.update({
|
|
245
|
-
id:
|
|
246
|
-
data: { status:
|
|
263
|
+
id: '123',
|
|
264
|
+
data: { status: 'done' },
|
|
247
265
|
});
|
|
248
266
|
```
|
|
249
267
|
|
|
250
268
|
### 5. `delete` - 删除
|
|
251
269
|
|
|
252
270
|
**输入**:
|
|
271
|
+
|
|
253
272
|
```typescript
|
|
254
|
-
{
|
|
273
|
+
{
|
|
274
|
+
id: string;
|
|
275
|
+
}
|
|
255
276
|
```
|
|
256
277
|
|
|
257
278
|
**输出**:
|
|
279
|
+
|
|
258
280
|
```typescript
|
|
259
281
|
void
|
|
260
282
|
```
|
|
261
283
|
|
|
262
284
|
**示例**:
|
|
285
|
+
|
|
263
286
|
```typescript
|
|
264
|
-
await trpc.tasks.delete({ id:
|
|
287
|
+
await trpc.tasks.delete({ id: '123' });
|
|
265
288
|
```
|
|
266
289
|
|
|
267
290
|
### 6. `deleteMany` - 批量删除
|
|
268
291
|
|
|
269
292
|
**输入**:
|
|
293
|
+
|
|
270
294
|
```typescript
|
|
271
295
|
{ ids: string[] }
|
|
272
296
|
```
|
|
273
297
|
|
|
274
298
|
**输出**:
|
|
299
|
+
|
|
275
300
|
```typescript
|
|
276
301
|
void
|
|
277
302
|
```
|
|
278
303
|
|
|
279
304
|
**示例**:
|
|
305
|
+
|
|
280
306
|
```typescript
|
|
281
|
-
await trpc.tasks.deleteMany({ ids: [
|
|
307
|
+
await trpc.tasks.deleteMany({ ids: ['1', '2', '3'] });
|
|
282
308
|
```
|
|
283
309
|
|
|
284
310
|
### 7. `updateMany` - 批量更新
|
|
285
311
|
|
|
286
312
|
**输入**:
|
|
313
|
+
|
|
287
314
|
```typescript
|
|
288
315
|
{
|
|
289
316
|
ids: string[];
|
|
@@ -292,50 +319,58 @@ await trpc.tasks.deleteMany({ ids: ["1", "2", "3"] });
|
|
|
292
319
|
```
|
|
293
320
|
|
|
294
321
|
**输出**:
|
|
322
|
+
|
|
295
323
|
```typescript
|
|
296
|
-
{
|
|
324
|
+
{
|
|
325
|
+
updated: number;
|
|
326
|
+
}
|
|
297
327
|
```
|
|
298
328
|
|
|
299
329
|
**示例**:
|
|
330
|
+
|
|
300
331
|
```typescript
|
|
301
332
|
await trpc.tasks.updateMany({
|
|
302
|
-
ids: [
|
|
303
|
-
data: { status:
|
|
333
|
+
ids: ['1', '2', '3'],
|
|
334
|
+
data: { status: 'done' },
|
|
304
335
|
});
|
|
305
336
|
```
|
|
306
337
|
|
|
307
338
|
### 8. `upsert` - 存在则更新,不存在则创建
|
|
308
339
|
|
|
309
340
|
**输入**:
|
|
341
|
+
|
|
310
342
|
```typescript
|
|
311
|
-
Omit<Task,
|
|
343
|
+
Omit<Task, 'createdAt' | 'updatedAt'>; // 需要包含 id
|
|
312
344
|
```
|
|
313
345
|
|
|
314
346
|
**输出**:
|
|
347
|
+
|
|
315
348
|
```typescript
|
|
316
349
|
{
|
|
317
|
-
data: Task;
|
|
318
|
-
isNew: boolean;
|
|
350
|
+
data: Task; // 创建或更新后的记录
|
|
351
|
+
isNew: boolean; // true = 新建, false = 更新
|
|
319
352
|
}
|
|
320
353
|
```
|
|
321
354
|
|
|
322
355
|
**示例**:
|
|
356
|
+
|
|
323
357
|
```typescript
|
|
324
358
|
// 如果 id="123" 存在则更新,不存在则创建
|
|
325
359
|
const { data, isNew } = await trpc.tasks.upsert({
|
|
326
|
-
id:
|
|
327
|
-
title:
|
|
328
|
-
status:
|
|
360
|
+
id: '123',
|
|
361
|
+
title: 'My Task',
|
|
362
|
+
status: 'todo',
|
|
329
363
|
});
|
|
330
364
|
|
|
331
365
|
if (isNew) {
|
|
332
|
-
console.log(
|
|
366
|
+
console.log('Created new task');
|
|
333
367
|
} else {
|
|
334
|
-
console.log(
|
|
368
|
+
console.log('Updated existing task');
|
|
335
369
|
}
|
|
336
370
|
```
|
|
337
371
|
|
|
338
372
|
**适用场景**:
|
|
373
|
+
|
|
339
374
|
- 同步外部数据
|
|
340
375
|
- 幂等导入
|
|
341
376
|
- 配置项更新
|
|
@@ -346,21 +381,21 @@ if (isNew) {
|
|
|
346
381
|
|
|
347
382
|
### 支持的操作符
|
|
348
383
|
|
|
349
|
-
| 操作符
|
|
350
|
-
|
|
351
|
-
| `eq`
|
|
352
|
-
| `ne`
|
|
353
|
-
| `gt`
|
|
354
|
-
| `gte`
|
|
355
|
-
| `lt`
|
|
356
|
-
| `lte`
|
|
357
|
-
| `like`
|
|
358
|
-
| `notLike`
|
|
359
|
-
| `in`
|
|
360
|
-
| `notIn`
|
|
361
|
-
| `between`
|
|
362
|
-
| `isNull`
|
|
363
|
-
| `isNotNull` | 不为空
|
|
384
|
+
| 操作符 | 说明 | 示例 |
|
|
385
|
+
| ----------- | ---------- | ------------------------------------------------- |
|
|
386
|
+
| `eq` | 等于 | `status = "done"` |
|
|
387
|
+
| `ne` | 不等于 | `status != "canceled"` |
|
|
388
|
+
| `gt` | 大于 | `estimatedHours > 5` |
|
|
389
|
+
| `gte` | 大于等于 | `estimatedHours >= 5` |
|
|
390
|
+
| `lt` | 小于 | `estimatedHours < 10` |
|
|
391
|
+
| `lte` | 小于等于 | `estimatedHours <= 10` |
|
|
392
|
+
| `like` | 包含 | `title LIKE "%bug%"` |
|
|
393
|
+
| `notLike` | 不包含 | `title NOT LIKE "%test%"` |
|
|
394
|
+
| `in` | 在列表中 | `status IN ["todo", "in-progress"]` |
|
|
395
|
+
| `notIn` | 不在列表中 | `status NOT IN ["canceled"]` |
|
|
396
|
+
| `between` | 范围 | `createdAt BETWEEN "2024-01-01" AND "2024-12-31"` |
|
|
397
|
+
| `isNull` | 为空 | `description IS NULL` |
|
|
398
|
+
| `isNotNull` | 不为空 | `description IS NOT NULL` |
|
|
364
399
|
|
|
365
400
|
### 过滤示例
|
|
366
401
|
|
|
@@ -370,9 +405,7 @@ if (isNew) {
|
|
|
370
405
|
await trpc.tasks.list({
|
|
371
406
|
page: 1,
|
|
372
407
|
perPage: 10,
|
|
373
|
-
filters: [
|
|
374
|
-
{ id: "status", value: "done", operator: "eq", variant: "select" },
|
|
375
|
-
],
|
|
408
|
+
filters: [{ id: 'status', value: 'done', operator: 'eq', variant: 'select' }],
|
|
376
409
|
});
|
|
377
410
|
```
|
|
378
411
|
|
|
@@ -383,10 +416,10 @@ await trpc.tasks.list({
|
|
|
383
416
|
page: 1,
|
|
384
417
|
perPage: 10,
|
|
385
418
|
filters: [
|
|
386
|
-
{ id:
|
|
387
|
-
{ id:
|
|
419
|
+
{ id: 'status', value: 'done', operator: 'eq', variant: 'select' },
|
|
420
|
+
{ id: 'priority', value: 'high', operator: 'eq', variant: 'select' },
|
|
388
421
|
],
|
|
389
|
-
joinOperator:
|
|
422
|
+
joinOperator: 'and', // status = "done" AND priority = "high"
|
|
390
423
|
});
|
|
391
424
|
```
|
|
392
425
|
|
|
@@ -397,10 +430,10 @@ await trpc.tasks.list({
|
|
|
397
430
|
page: 1,
|
|
398
431
|
perPage: 10,
|
|
399
432
|
filters: [
|
|
400
|
-
{ id:
|
|
401
|
-
{ id:
|
|
433
|
+
{ id: 'status', value: 'todo', operator: 'eq', variant: 'select' },
|
|
434
|
+
{ id: 'status', value: 'in-progress', operator: 'eq', variant: 'select' },
|
|
402
435
|
],
|
|
403
|
-
joinOperator:
|
|
436
|
+
joinOperator: 'or', // status = "todo" OR status = "in-progress"
|
|
404
437
|
});
|
|
405
438
|
```
|
|
406
439
|
|
|
@@ -412,10 +445,10 @@ await trpc.tasks.list({
|
|
|
412
445
|
perPage: 10,
|
|
413
446
|
filters: [
|
|
414
447
|
{
|
|
415
|
-
id:
|
|
416
|
-
value: [
|
|
417
|
-
operator:
|
|
418
|
-
variant:
|
|
448
|
+
id: 'createdAt',
|
|
449
|
+
value: ['2024-01-01', '2024-12-31'],
|
|
450
|
+
operator: 'between',
|
|
451
|
+
variant: 'dateRange',
|
|
419
452
|
},
|
|
420
453
|
],
|
|
421
454
|
});
|
|
@@ -427,9 +460,7 @@ await trpc.tasks.list({
|
|
|
427
460
|
await trpc.tasks.list({
|
|
428
461
|
page: 1,
|
|
429
462
|
perPage: 10,
|
|
430
|
-
filters: [
|
|
431
|
-
{ id: "title", value: "bug", operator: "like", variant: "text" },
|
|
432
|
-
],
|
|
463
|
+
filters: [{ id: 'title', value: 'bug', operator: 'like', variant: 'text' }],
|
|
433
464
|
});
|
|
434
465
|
```
|
|
435
466
|
|
|
@@ -446,25 +477,28 @@ await trpc.tasks.list({
|
|
|
446
477
|
```typescript
|
|
447
478
|
interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
|
|
448
479
|
// ========== 必填 ==========
|
|
449
|
-
table: TTable;
|
|
480
|
+
table: TTable; // Drizzle 表定义
|
|
450
481
|
|
|
451
482
|
// ========== Schema 配置(可选) ==========
|
|
452
|
-
schema?: z.ZodType<TInsert>;
|
|
453
|
-
updateSchema?: z.ZodType<TUpdate
|
|
454
|
-
selectSchema?: z.ZodType<TSelect
|
|
483
|
+
schema?: z.ZodType<TInsert>; // 主 Schema(用于 create/upsert)
|
|
484
|
+
updateSchema?: z.ZodType<TUpdate>; // 更新 Schema(覆盖自动派生)
|
|
485
|
+
selectSchema?: z.ZodType<TSelect>; // 查询返回 Schema
|
|
486
|
+
listInputSchema?: z.ZodType; // list 输入 Schema(可扩展)
|
|
487
|
+
getInputSchema?: z.ZodType; // get 输入 Schema(可扩展)
|
|
488
|
+
exportInputSchema?: z.ZodType; // export 输入 Schema(可扩展)
|
|
455
489
|
|
|
456
490
|
// ========== 其他配置 ==========
|
|
457
|
-
idField?: string;
|
|
458
|
-
omitFields?: string[];
|
|
459
|
-
|
|
491
|
+
idField?: string; // ID 字段名,默认 "id"
|
|
492
|
+
omitFields?: string[]; // 自动派生时排除的字段
|
|
493
|
+
// 默认 ["id", "createdAt", "updatedAt"]
|
|
460
494
|
}
|
|
461
495
|
```
|
|
462
496
|
|
|
463
497
|
#### Schema 派生规则
|
|
464
498
|
|
|
465
|
-
| 配置
|
|
466
|
-
|
|
467
|
-
| 无 `schema`
|
|
499
|
+
| 配置 | 派生行为 |
|
|
500
|
+
| ----------------- | ------------------------------------------- |
|
|
501
|
+
| 无 `schema` | 从 `table` 自动派生,排除 `omitFields` |
|
|
468
502
|
| 无 `updateSchema` | 从 `schema.partial().refine(nonEmpty)` 派生 |
|
|
469
503
|
| 无 `selectSchema` | 若有 `schema` 则使用它,否则从 `table` 派生 |
|
|
470
504
|
|
|
@@ -492,10 +526,121 @@ const usersRouter = createCrudRouter({
|
|
|
492
526
|
// 4. 自定义排除字段
|
|
493
527
|
const ordersRouter = createCrudRouter({
|
|
494
528
|
table: orders,
|
|
495
|
-
omitFields: [
|
|
529
|
+
omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 扩展读取输入
|
|
534
|
+
|
|
535
|
+
默认 `list` 输入为 `baseListInputSchema`:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
{
|
|
539
|
+
page: number;
|
|
540
|
+
perPage: number;
|
|
541
|
+
sort?: Array<{ id: string; desc: boolean }>;
|
|
542
|
+
filters?: Array<FilterItem>;
|
|
543
|
+
joinOperator: "and" | "or";
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
业务侧可以用 `baseListInputSchema.extend(...)` 增加自定义参数,不需要覆盖
|
|
548
|
+
`list` procedure。默认查询逻辑仍只读取分页、排序、过滤和 `joinOperator`,
|
|
549
|
+
额外字段会保留在 `middleware.list` 的 `input` 中。
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { baseListInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
553
|
+
import { z } from 'zod';
|
|
554
|
+
|
|
555
|
+
const productsRouter = createCrudRouter({
|
|
556
|
+
table: shopProducts,
|
|
557
|
+
idField: 'spuId',
|
|
558
|
+
schema: createProductSchema,
|
|
559
|
+
updateSchema: updateProductSchema,
|
|
560
|
+
listInputSchema: baseListInputSchema.extend({
|
|
561
|
+
include: z
|
|
562
|
+
.object({
|
|
563
|
+
skus: z.boolean().optional(),
|
|
564
|
+
})
|
|
565
|
+
.optional(),
|
|
566
|
+
}),
|
|
567
|
+
middleware: {
|
|
568
|
+
list: async ({ ctx, input, next }) => {
|
|
569
|
+
const result = await next(input);
|
|
570
|
+
if (!input.include?.skus) return result;
|
|
571
|
+
return attachSkus(ctx, result);
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// 调用方
|
|
577
|
+
await trpc.products.list.query({
|
|
578
|
+
page: 1,
|
|
579
|
+
perPage: 20,
|
|
580
|
+
include: { skus: true },
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
`get` 也可以扩展。默认仍兼容 `get("spu_1")`,业务侧如需详情页按需挂载关联数据,可以改成对象输入:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { baseGetInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
588
|
+
import { z } from 'zod';
|
|
589
|
+
|
|
590
|
+
const productsRouter = createCrudRouter({
|
|
591
|
+
table: shopProducts,
|
|
592
|
+
idField: 'spuId',
|
|
593
|
+
schema: createProductSchema,
|
|
594
|
+
updateSchema: updateProductSchema,
|
|
595
|
+
getInputSchema: baseGetInputSchema.extend({
|
|
596
|
+
include: z.object({ skus: z.boolean().optional() }).optional(),
|
|
597
|
+
}),
|
|
598
|
+
middleware: {
|
|
599
|
+
get: async ({ ctx, input, next }) => {
|
|
600
|
+
const product = await next(input);
|
|
601
|
+
if (!product || typeof input === 'string' || !input.include?.skus) {
|
|
602
|
+
return product;
|
|
603
|
+
}
|
|
604
|
+
return attachProductSkus(ctx, product);
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
await trpc.products.get.query({
|
|
610
|
+
id: 'spu_1',
|
|
611
|
+
include: { skus: true },
|
|
612
|
+
});
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
`export` 支持同样的扩展方式,默认导出逻辑只读取 `sort`、`filters`、`joinOperator` 和 `limit`:
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
import { baseExportInputSchema, createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
619
|
+
import { z } from 'zod';
|
|
620
|
+
|
|
621
|
+
const productsRouter = createCrudRouter({
|
|
622
|
+
table: shopProducts,
|
|
623
|
+
schema: createProductSchema,
|
|
624
|
+
updateSchema: updateProductSchema,
|
|
625
|
+
exportInputSchema: baseExportInputSchema.extend({
|
|
626
|
+
format: z.enum(['csv', 'xlsx']).optional(),
|
|
627
|
+
}),
|
|
628
|
+
middleware: {
|
|
629
|
+
export: async ({ input, next }) => {
|
|
630
|
+
const result = await next(input);
|
|
631
|
+
return input.format === 'xlsx' ? toXlsxExport(result) : result;
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await trpc.products.export.query({
|
|
637
|
+
limit: 1000,
|
|
638
|
+
format: 'xlsx',
|
|
496
639
|
});
|
|
497
640
|
```
|
|
498
641
|
|
|
642
|
+
写入类内置方法(`create`、`update`、`upsert`、`createMany`)不提供通用控制参数扩展;它们的输入字段会进入默认写入逻辑。需要 `dryRun`、`notify` 等控制参数时,建议单独设计 envelope 或自定义业务 procedure。
|
|
643
|
+
|
|
499
644
|
#### 返回值
|
|
500
645
|
|
|
501
646
|
```typescript
|
|
@@ -507,7 +652,7 @@ const ordersRouter = createCrudRouter({
|
|
|
507
652
|
// 可 spread 的 procedures 对象(用于扩展自定义路由)
|
|
508
653
|
procedures: {
|
|
509
654
|
list: Procedure<ListInput, ListOutput>,
|
|
510
|
-
get: Procedure<string, TSelect>,
|
|
655
|
+
get: Procedure<string | { id: string, ...extra }, TSelect>,
|
|
511
656
|
create: Procedure<TInsert, TSelect>,
|
|
512
657
|
update: Procedure<{ id: string, data: TUpdate }, TSelect>,
|
|
513
658
|
delete: Procedure<string, TSelect>,
|
|
@@ -525,18 +670,18 @@ const ordersRouter = createCrudRouter({
|
|
|
525
670
|
### 使用 procedure 配置
|
|
526
671
|
|
|
527
672
|
```typescript
|
|
528
|
-
import { createCrudRouter } from
|
|
529
|
-
import { protectedProcedure, adminProcedure, publicProcedure } from
|
|
673
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
674
|
+
import { protectedProcedure, adminProcedure, publicProcedure } from '../trpc';
|
|
530
675
|
|
|
531
676
|
export const tasksRouter = createCrudRouter({
|
|
532
677
|
table: tasks,
|
|
533
678
|
// 按操作指定不同的 procedure
|
|
534
679
|
procedure: {
|
|
535
|
-
list: publicProcedure,
|
|
680
|
+
list: publicProcedure, // 公开读
|
|
536
681
|
get: publicProcedure,
|
|
537
682
|
create: protectedProcedure, // 需要登录
|
|
538
683
|
update: protectedProcedure,
|
|
539
|
-
delete: adminProcedure,
|
|
684
|
+
delete: adminProcedure, // 需要管理员
|
|
540
685
|
default: protectedProcedure,
|
|
541
686
|
},
|
|
542
687
|
});
|
|
@@ -549,8 +694,8 @@ export const tasksRouter = createCrudRouter({
|
|
|
549
694
|
table: tasks,
|
|
550
695
|
guard: (ctx, operation) => {
|
|
551
696
|
// 删除操作需要管理员权限
|
|
552
|
-
if (operation ===
|
|
553
|
-
return ctx.user.role ===
|
|
697
|
+
if (operation === 'delete') {
|
|
698
|
+
return ctx.user.role === 'admin';
|
|
554
699
|
}
|
|
555
700
|
// 其他操作需要登录
|
|
556
701
|
return !!ctx.user;
|
|
@@ -561,7 +706,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
561
706
|
### 使用 scope(行级过滤 RLS)
|
|
562
707
|
|
|
563
708
|
```typescript
|
|
564
|
-
import { eq } from
|
|
709
|
+
import { eq } from 'drizzle-orm';
|
|
565
710
|
|
|
566
711
|
export const tasksRouter = createCrudRouter({
|
|
567
712
|
table: tasks,
|
|
@@ -592,7 +737,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
592
737
|
// 写入时强制覆盖字段(防止伪造)
|
|
593
738
|
inject: (ctx, operation) => ({
|
|
594
739
|
tenantId: ctx.user.tenantId,
|
|
595
|
-
...(operation ===
|
|
740
|
+
...(operation === 'create' ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
|
|
596
741
|
}),
|
|
597
742
|
});
|
|
598
743
|
```
|
|
@@ -606,9 +751,9 @@ export const tasksRouter = createCrudRouter({
|
|
|
606
751
|
使用 `.procedures` 属性 spread 出 CRUD 路由,然后添加自定义路由:
|
|
607
752
|
|
|
608
753
|
```typescript
|
|
609
|
-
import { createCrudRouter, router } from
|
|
610
|
-
import { protectedProcedure } from
|
|
611
|
-
import { z } from
|
|
754
|
+
import { createCrudRouter, router } from '@wordrhyme/auto-crud-server';
|
|
755
|
+
import { protectedProcedure } from '../trpc';
|
|
756
|
+
import { z } from 'zod';
|
|
612
757
|
|
|
613
758
|
// 创建基础 CRUD
|
|
614
759
|
const tasksCrud = createCrudRouter({
|
|
@@ -623,20 +768,14 @@ export const tasksRouter = router({
|
|
|
623
768
|
archive: protectedProcedure
|
|
624
769
|
.input(z.object({ id: z.string() }))
|
|
625
770
|
.mutation(async ({ input, ctx }) => {
|
|
626
|
-
return ctx.db
|
|
627
|
-
.update(tasks)
|
|
628
|
-
.set({ archived: true })
|
|
629
|
-
.where(eq(tasks.id, input.id));
|
|
771
|
+
return ctx.db.update(tasks).set({ archived: true }).where(eq(tasks.id, input.id));
|
|
630
772
|
}),
|
|
631
773
|
|
|
632
774
|
// 自定义路由:取消归档
|
|
633
775
|
unarchive: protectedProcedure
|
|
634
776
|
.input(z.object({ id: z.string() }))
|
|
635
777
|
.mutation(async ({ input, ctx }) => {
|
|
636
|
-
return ctx.db
|
|
637
|
-
.update(tasks)
|
|
638
|
-
.set({ archived: false })
|
|
639
|
-
.where(eq(tasks.id, input.id));
|
|
778
|
+
return ctx.db.update(tasks).set({ archived: false }).where(eq(tasks.id, input.id));
|
|
640
779
|
}),
|
|
641
780
|
});
|
|
642
781
|
```
|
|
@@ -659,27 +798,21 @@ export const appRouter = router({
|
|
|
659
798
|
### 自定义过滤逻辑
|
|
660
799
|
|
|
661
800
|
```typescript
|
|
662
|
-
//
|
|
663
|
-
const tasksCrud = createCrudRouter({
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.input(listInputSchema)
|
|
671
|
-
.query(async ({ input, ctx }) => {
|
|
672
|
-
// 自定义过滤逻辑
|
|
673
|
-
const customFilters = input.filters?.map(filter => {
|
|
674
|
-
if (filter.id === "customField") {
|
|
675
|
-
return { ...filter, operator: "custom" };
|
|
801
|
+
// 通过 middleware 调整输入,继续复用内置分页/排序/过滤/count/scope/guard
|
|
802
|
+
const tasksCrud = createCrudRouter({
|
|
803
|
+
table: tasks,
|
|
804
|
+
middleware: {
|
|
805
|
+
list: async ({ input, next }) => {
|
|
806
|
+
const filters = input.filters?.map((filter) => {
|
|
807
|
+
if (filter.id === 'customField') {
|
|
808
|
+
return { ...filter, operator: 'custom' };
|
|
676
809
|
}
|
|
677
810
|
return filter;
|
|
678
811
|
});
|
|
679
812
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
813
|
+
return next({ ...input, filters });
|
|
814
|
+
},
|
|
815
|
+
},
|
|
683
816
|
});
|
|
684
817
|
```
|
|
685
818
|
|
|
@@ -692,7 +825,7 @@ export const tasksRouter = router({
|
|
|
692
825
|
### 完整控制模式
|
|
693
826
|
|
|
694
827
|
```typescript
|
|
695
|
-
import { createCrudRouter } from
|
|
828
|
+
import { createCrudRouter } from '@wordrhyme/auto-crud-server';
|
|
696
829
|
|
|
697
830
|
const tasksRouter = createCrudRouter({
|
|
698
831
|
table: tasks,
|
|
@@ -709,7 +842,7 @@ const tasksRouter = createCrudRouter({
|
|
|
709
842
|
|
|
710
843
|
// 3. 执行副作用
|
|
711
844
|
await sendNotification(result);
|
|
712
|
-
await logAudit(ctx.user,
|
|
845
|
+
await logAudit(ctx.user, 'create', result);
|
|
713
846
|
|
|
714
847
|
// 4. 返回结果(可修改)
|
|
715
848
|
return result;
|
|
@@ -719,15 +852,15 @@ const tasksRouter = createCrudRouter({
|
|
|
719
852
|
update: async ({ ctx, id, data, existing, next }) => {
|
|
720
853
|
// 检查权限
|
|
721
854
|
if (existing.ownerId !== ctx.user.id) {
|
|
722
|
-
throw new Error(
|
|
855
|
+
throw new Error('Forbidden');
|
|
723
856
|
}
|
|
724
857
|
return next(data);
|
|
725
858
|
},
|
|
726
859
|
|
|
727
860
|
// 删除:条件拦截
|
|
728
861
|
delete: async ({ ctx, id, existing, next }) => {
|
|
729
|
-
if (existing.status ===
|
|
730
|
-
throw new Error(
|
|
862
|
+
if (existing.status === 'locked') {
|
|
863
|
+
throw new Error('Cannot delete locked resource');
|
|
731
864
|
}
|
|
732
865
|
return next();
|
|
733
866
|
},
|
|
@@ -746,7 +879,7 @@ import {
|
|
|
746
879
|
beforeMiddleware,
|
|
747
880
|
afterCreate,
|
|
748
881
|
beforeCreate,
|
|
749
|
-
} from
|
|
882
|
+
} from '@wordrhyme/auto-crud-server';
|
|
750
883
|
|
|
751
884
|
const tasksRouter = createCrudRouter({
|
|
752
885
|
table: tasks,
|
|
@@ -755,7 +888,7 @@ const tasksRouter = createCrudRouter({
|
|
|
755
888
|
// 简单副作用:只在操作后执行
|
|
756
889
|
create: afterMiddleware(async (ctx, result) => {
|
|
757
890
|
await sendEmail(result);
|
|
758
|
-
await logAudit(ctx.user,
|
|
891
|
+
await logAudit(ctx.user, 'create', result);
|
|
759
892
|
}),
|
|
760
893
|
|
|
761
894
|
// 修改输入:只在操作前执行
|
|
@@ -773,15 +906,15 @@ const tasksRouter = createCrudRouter({
|
|
|
773
906
|
|
|
774
907
|
### 可用的工具函数
|
|
775
908
|
|
|
776
|
-
| 函数
|
|
777
|
-
|
|
778
|
-
| `afterMiddleware(fn)`
|
|
779
|
-
| `afterMiddlewareTransform(fn)` | 操作后修改返回值
|
|
780
|
-
| `beforeMiddleware(fn)`
|
|
781
|
-
| `composeMiddleware(...fns)`
|
|
782
|
-
| `afterList`, `beforeList`
|
|
783
|
-
| `afterCreate`, `beforeCreate`
|
|
784
|
-
| `afterUpdate`, `afterDelete`
|
|
909
|
+
| 函数 | 用途 | 示例 |
|
|
910
|
+
| ------------------------------ | ------------------ | -------------------- |
|
|
911
|
+
| `afterMiddleware(fn)` | 操作后执行副作用 | 日志、通知、审计 |
|
|
912
|
+
| `afterMiddlewareTransform(fn)` | 操作后修改返回值 | 添加计算字段 |
|
|
913
|
+
| `beforeMiddleware(fn)` | 操作前修改输入 | 注入用户ID、生成slug |
|
|
914
|
+
| `composeMiddleware(...fns)` | 组合多个中间件 | 复杂场景 |
|
|
915
|
+
| `afterList`, `beforeList` | list 操作专用 | 分页后处理 |
|
|
916
|
+
| `afterCreate`, `beforeCreate` | create 操作专用 | 创建通知 |
|
|
917
|
+
| `afterUpdate`, `afterDelete` | update/delete 专用 | 更新/删除通知 |
|
|
785
918
|
|
|
786
919
|
---
|
|
787
920
|
|
|
@@ -790,9 +923,9 @@ const tasksRouter = createCrudRouter({
|
|
|
790
923
|
### 与 Drizzle ORM 集成
|
|
791
924
|
|
|
792
925
|
```typescript
|
|
793
|
-
import { drizzle } from
|
|
794
|
-
import postgres from
|
|
795
|
-
import * as schema from
|
|
926
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
927
|
+
import postgres from 'postgres';
|
|
928
|
+
import * as schema from './schema';
|
|
796
929
|
|
|
797
930
|
const client = postgres(process.env.DATABASE_URL!);
|
|
798
931
|
export const db = drizzle(client, { schema });
|
|
@@ -805,12 +938,12 @@ export const createContext = () => ({ db });
|
|
|
805
938
|
|
|
806
939
|
```typescript
|
|
807
940
|
// app/api/trpc/[trpc]/route.ts
|
|
808
|
-
import { fetchRequestHandler } from
|
|
809
|
-
import { appRouter } from
|
|
941
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
942
|
+
import { appRouter } from '@/server/routers';
|
|
810
943
|
|
|
811
944
|
const handler = (req: Request) =>
|
|
812
945
|
fetchRequestHandler({
|
|
813
|
-
endpoint:
|
|
946
|
+
endpoint: '/api/trpc',
|
|
814
947
|
req,
|
|
815
948
|
router: appRouter,
|
|
816
949
|
createContext: () => ({ db }),
|
|
@@ -822,18 +955,18 @@ export { handler as GET, handler as POST };
|
|
|
822
955
|
### 与 Express 集成
|
|
823
956
|
|
|
824
957
|
```typescript
|
|
825
|
-
import express from
|
|
826
|
-
import { createExpressMiddleware } from
|
|
827
|
-
import { appRouter } from
|
|
958
|
+
import express from 'express';
|
|
959
|
+
import { createExpressMiddleware } from '@trpc/server/adapters/express';
|
|
960
|
+
import { appRouter } from './server/routers';
|
|
828
961
|
|
|
829
962
|
const app = express();
|
|
830
963
|
|
|
831
964
|
app.use(
|
|
832
|
-
|
|
965
|
+
'/trpc',
|
|
833
966
|
createExpressMiddleware({
|
|
834
967
|
router: appRouter,
|
|
835
968
|
createContext: () => ({ db }),
|
|
836
|
-
})
|
|
969
|
+
}),
|
|
837
970
|
);
|
|
838
971
|
|
|
839
972
|
app.listen(3000);
|
|
@@ -848,7 +981,7 @@ app.listen(3000);
|
|
|
848
981
|
```typescript
|
|
849
982
|
export const usersRouter = createCrudRouter({
|
|
850
983
|
table: users,
|
|
851
|
-
idField:
|
|
984
|
+
idField: 'userId', // 使用自定义 ID 字段
|
|
852
985
|
});
|
|
853
986
|
```
|
|
854
987
|
|
|
@@ -858,7 +991,7 @@ export const usersRouter = createCrudRouter({
|
|
|
858
991
|
export const ordersRouter = createCrudRouter({
|
|
859
992
|
table: orders,
|
|
860
993
|
// 自动派生 schema 时排除这些字段
|
|
861
|
-
omitFields: [
|
|
994
|
+
omitFields: ['id', 'createdAt', 'updatedAt', 'internalCode'],
|
|
862
995
|
});
|
|
863
996
|
```
|
|
864
997
|
|
|
@@ -871,10 +1004,10 @@ export const tasksRouter = createCrudRouter({
|
|
|
871
1004
|
softDelete: true,
|
|
872
1005
|
|
|
873
1006
|
// 方式 2:指定列名
|
|
874
|
-
softDelete:
|
|
1007
|
+
softDelete: 'deletedAt',
|
|
875
1008
|
|
|
876
1009
|
// 方式 3:完整配置(用于布尔字段)
|
|
877
|
-
softDelete: { column:
|
|
1010
|
+
softDelete: { column: 'isDeleted', value: () => true },
|
|
878
1011
|
});
|
|
879
1012
|
```
|
|
880
1013
|
|
|
@@ -883,7 +1016,7 @@ export const tasksRouter = createCrudRouter({
|
|
|
883
1016
|
```typescript
|
|
884
1017
|
export const tasksRouter = createCrudRouter({
|
|
885
1018
|
table: tasks,
|
|
886
|
-
maxBatchSize: 50,
|
|
1019
|
+
maxBatchSize: 50, // 默认 100
|
|
887
1020
|
});
|
|
888
1021
|
```
|
|
889
1022
|
|
|
@@ -892,8 +1025,8 @@ export const tasksRouter = createCrudRouter({
|
|
|
892
1025
|
```typescript
|
|
893
1026
|
export const tasksRouter = createCrudRouter({
|
|
894
1027
|
table: tasks,
|
|
895
|
-
filterableColumns: [
|
|
896
|
-
sortableColumns: [
|
|
1028
|
+
filterableColumns: ['title', 'status', 'priority'], // 只允许这些列过滤
|
|
1029
|
+
sortableColumns: ['title', 'createdAt', 'priority'], // 只允许这些列排序
|
|
897
1030
|
});
|
|
898
1031
|
```
|
|
899
1032
|
|
|
@@ -903,13 +1036,21 @@ export const tasksRouter = createCrudRouter({
|
|
|
903
1036
|
|
|
904
1037
|
```typescript
|
|
905
1038
|
// 主要导出
|
|
906
|
-
export {
|
|
1039
|
+
export {
|
|
1040
|
+
baseExportInputSchema,
|
|
1041
|
+
baseGetInputSchema,
|
|
1042
|
+
baseListInputSchema,
|
|
1043
|
+
createCrudRouter,
|
|
1044
|
+
} from './routers/_factory';
|
|
907
1045
|
export type {
|
|
908
1046
|
CrudRouterConfig,
|
|
909
1047
|
CrudMiddleware,
|
|
1048
|
+
GetInput,
|
|
910
1049
|
ListInput,
|
|
911
1050
|
ListResult,
|
|
912
|
-
|
|
1051
|
+
ExportInput,
|
|
1052
|
+
ExportResult,
|
|
1053
|
+
} from './types/config';
|
|
913
1054
|
|
|
914
1055
|
// Middleware 工具函数
|
|
915
1056
|
export {
|
|
@@ -923,14 +1064,14 @@ export {
|
|
|
923
1064
|
beforeCreate,
|
|
924
1065
|
afterUpdate,
|
|
925
1066
|
afterDelete,
|
|
926
|
-
} from
|
|
1067
|
+
} from './lib/middleware-helpers';
|
|
927
1068
|
|
|
928
1069
|
// tRPC 工具
|
|
929
|
-
export { router, publicProcedure } from
|
|
1070
|
+
export { router, publicProcedure } from './trpc';
|
|
930
1071
|
|
|
931
1072
|
// 示例路由(可选)
|
|
932
|
-
export { appRouter } from
|
|
933
|
-
export type { AppRouter } from
|
|
1073
|
+
export { appRouter } from './routers';
|
|
1074
|
+
export type { AppRouter } from './routers';
|
|
934
1075
|
```
|
|
935
1076
|
|
|
936
1077
|
---
|
|
@@ -942,6 +1083,7 @@ export type { AppRouter } from "./routers";
|
|
|
942
1083
|
**错误**: `Cannot find module '@wordrhyme/auto-crud-server'`
|
|
943
1084
|
|
|
944
1085
|
**解决方案**:
|
|
1086
|
+
|
|
945
1087
|
```bash
|
|
946
1088
|
pnpm install @wordrhyme/auto-crud-server
|
|
947
1089
|
```
|
|
@@ -951,6 +1093,7 @@ pnpm install @wordrhyme/auto-crud-server
|
|
|
951
1093
|
**错误**: `Type 'X' is not assignable to type 'Y'`
|
|
952
1094
|
|
|
953
1095
|
**解决方案**: 确保 Zod 版本一致
|
|
1096
|
+
|
|
954
1097
|
```bash
|
|
955
1098
|
pnpm list zod
|
|
956
1099
|
# 确保所有包使用相同的 Zod 版本
|
|
@@ -961,9 +1104,10 @@ pnpm list zod
|
|
|
961
1104
|
**错误**: `connect ECONNREFUSED`
|
|
962
1105
|
|
|
963
1106
|
**解决方案**: 检查数据库连接字符串
|
|
1107
|
+
|
|
964
1108
|
```typescript
|
|
965
1109
|
// .env
|
|
966
|
-
DATABASE_URL=
|
|
1110
|
+
DATABASE_URL = 'postgresql://user:password@localhost:5432/dbname';
|
|
967
1111
|
```
|
|
968
1112
|
|
|
969
1113
|
---
|