@spfn/core 0.2.0-beta.1 → 0.2.0-beta.10
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 +262 -1092
- package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +159 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +15 -6
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +13 -0
- package/dist/db/index.js +40 -6
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +82 -3
- package/dist/env/index.js +81 -14
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/event/index.d.ts +3 -70
- package/dist/event/index.js +10 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +54 -8
- package/dist/job/index.js +61 -12
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -11
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +36 -4
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +62 -15
- package/dist/nextjs/server.js +102 -33
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +227 -15
- package/dist/route/index.js +307 -31
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +2 -31
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +153 -6
- package/dist/server/index.js +216 -14
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/{types-DRG2XMTR.d.ts → types-BOPTApC2.d.ts} +91 -3
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +18 -3
package/docs/entity.md
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# Entity
|
|
2
|
+
|
|
3
|
+
Database schema definition with Drizzle ORM and reusable column helpers.
|
|
4
|
+
|
|
5
|
+
## Basic Entity
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/server/entities/users.ts
|
|
9
|
+
import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
|
|
10
|
+
import { id, timestamps } from '@spfn/core/db';
|
|
11
|
+
|
|
12
|
+
export const users = pgTable('users', {
|
|
13
|
+
id: id(),
|
|
14
|
+
email: text('email').notNull().unique(),
|
|
15
|
+
name: text('name').notNull(),
|
|
16
|
+
role: text('role', { enum: ['admin', 'user', 'guest'] }).notNull().default('user'),
|
|
17
|
+
isActive: boolean('is_active').notNull().default(true),
|
|
18
|
+
...timestamps()
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Type inference
|
|
22
|
+
export type User = typeof users.$inferSelect;
|
|
23
|
+
export type NewUser = typeof users.$inferInsert;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Column Helpers
|
|
29
|
+
|
|
30
|
+
### id()
|
|
31
|
+
|
|
32
|
+
Auto-incrementing bigserial primary key.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { id } from '@spfn/core/db';
|
|
36
|
+
|
|
37
|
+
export const users = pgTable('users', {
|
|
38
|
+
id: id(), // bigserial primary key
|
|
39
|
+
...
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### uuid()
|
|
44
|
+
|
|
45
|
+
UUID primary key with auto-generation.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { uuid } from '@spfn/core/db';
|
|
49
|
+
|
|
50
|
+
export const sessions = pgTable('sessions', {
|
|
51
|
+
id: uuid(), // uuid with gen_random_uuid()
|
|
52
|
+
...
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### timestamps()
|
|
57
|
+
|
|
58
|
+
Standard createdAt/updatedAt fields.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { timestamps } from '@spfn/core/db';
|
|
62
|
+
|
|
63
|
+
export const users = pgTable('users', {
|
|
64
|
+
id: id(),
|
|
65
|
+
name: text('name'),
|
|
66
|
+
...timestamps()
|
|
67
|
+
// createdAt: timestamptz, not null, default now()
|
|
68
|
+
// updatedAt: timestamptz, not null, default now()
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### foreignKey()
|
|
73
|
+
|
|
74
|
+
Required foreign key with cascade delete (default).
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { foreignKey } from '@spfn/core/db';
|
|
78
|
+
|
|
79
|
+
export const posts = pgTable('posts', {
|
|
80
|
+
id: id(),
|
|
81
|
+
authorId: foreignKey('author', () => users.id),
|
|
82
|
+
// author_id bigserial NOT NULL REFERENCES users(id) ON DELETE CASCADE
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Custom options
|
|
86
|
+
authorId: foreignKey('author', () => users.id, {
|
|
87
|
+
onDelete: 'set null',
|
|
88
|
+
onUpdate: 'cascade'
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### optionalForeignKey()
|
|
93
|
+
|
|
94
|
+
Nullable foreign key with set null (default).
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { optionalForeignKey } from '@spfn/core/db';
|
|
98
|
+
|
|
99
|
+
export const posts = pgTable('posts', {
|
|
100
|
+
id: id(),
|
|
101
|
+
categoryId: optionalForeignKey('category', () => categories.id),
|
|
102
|
+
// category_id bigserial REFERENCES categories(id) ON DELETE SET NULL
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### auditFields()
|
|
107
|
+
|
|
108
|
+
User tracking fields.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { auditFields } from '@spfn/core/db';
|
|
112
|
+
|
|
113
|
+
export const posts = pgTable('posts', {
|
|
114
|
+
id: id(),
|
|
115
|
+
...auditFields()
|
|
116
|
+
// createdBy: text (nullable)
|
|
117
|
+
// updatedBy: text (nullable)
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### publishingFields()
|
|
122
|
+
|
|
123
|
+
Content publishing fields.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { publishingFields } from '@spfn/core/db';
|
|
127
|
+
|
|
128
|
+
export const articles = pgTable('articles', {
|
|
129
|
+
id: id(),
|
|
130
|
+
...publishingFields()
|
|
131
|
+
// publishedAt: timestamptz (nullable)
|
|
132
|
+
// publishedBy: text (nullable)
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### softDelete()
|
|
137
|
+
|
|
138
|
+
Soft deletion fields.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { softDelete } from '@spfn/core/db';
|
|
142
|
+
|
|
143
|
+
export const posts = pgTable('posts', {
|
|
144
|
+
id: id(),
|
|
145
|
+
...softDelete()
|
|
146
|
+
// deletedAt: timestamptz (nullable)
|
|
147
|
+
// deletedBy: text (nullable)
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### verificationTimestamp()
|
|
152
|
+
|
|
153
|
+
Custom verification timestamp.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { verificationTimestamp } from '@spfn/core/db';
|
|
157
|
+
|
|
158
|
+
export const users = pgTable('users', {
|
|
159
|
+
id: id(),
|
|
160
|
+
...verificationTimestamp('emailVerified'), // emailVerifiedAt
|
|
161
|
+
...verificationTimestamp('phoneVerified'), // phoneVerifiedAt
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### utcTimestamp()
|
|
166
|
+
|
|
167
|
+
UTC timestamp field.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { utcTimestamp } from '@spfn/core/db';
|
|
171
|
+
|
|
172
|
+
export const events = pgTable('events', {
|
|
173
|
+
id: id(),
|
|
174
|
+
scheduledAt: utcTimestamp('scheduled_at').notNull(),
|
|
175
|
+
processedAt: utcTimestamp('processed_at', 'string'), // ISO string mode
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### enumText()
|
|
180
|
+
|
|
181
|
+
Type-safe enum text field.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { enumText } from '@spfn/core/db';
|
|
185
|
+
|
|
186
|
+
const USER_STATUSES = ['active', 'inactive', 'suspended'] as const;
|
|
187
|
+
|
|
188
|
+
export const users = pgTable('users', {
|
|
189
|
+
id: id(),
|
|
190
|
+
status: enumText('status', USER_STATUSES).default('active').notNull(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// TypeScript type: 'active' | 'inactive' | 'suspended'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### typedJsonb()
|
|
197
|
+
|
|
198
|
+
Type-safe JSONB field.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { typedJsonb } from '@spfn/core/db';
|
|
202
|
+
|
|
203
|
+
type UserMetadata = {
|
|
204
|
+
preferences: { theme: 'light' | 'dark' };
|
|
205
|
+
settings: Record<string, any>;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const users = pgTable('users', {
|
|
209
|
+
id: id(),
|
|
210
|
+
metadata: typedJsonb<UserMetadata>('metadata').notNull(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// user.metadata.preferences.theme is typed
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Indexes & Constraints
|
|
219
|
+
|
|
220
|
+
### Column-level Constraints
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { pgTable, text, integer, boolean } from 'drizzle-orm/pg-core';
|
|
224
|
+
|
|
225
|
+
export const users = pgTable('users', {
|
|
226
|
+
id: id(),
|
|
227
|
+
email: text('email').notNull().unique(), // UNIQUE constraint
|
|
228
|
+
age: integer('age').notNull().default(0), // DEFAULT constraint
|
|
229
|
+
isActive: boolean('is_active').notNull(), // NOT NULL constraint
|
|
230
|
+
...timestamps()
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Table-level Indexes
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { pgTable, text, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
|
238
|
+
|
|
239
|
+
export const users = pgTable('users', {
|
|
240
|
+
id: id(),
|
|
241
|
+
email: text('email').notNull(),
|
|
242
|
+
name: text('name').notNull(),
|
|
243
|
+
organizationId: text('organization_id'),
|
|
244
|
+
...timestamps()
|
|
245
|
+
}, (table) => [
|
|
246
|
+
// Single column index
|
|
247
|
+
index('users_email_idx').on(table.email),
|
|
248
|
+
|
|
249
|
+
// Unique index
|
|
250
|
+
uniqueIndex('users_email_unique').on(table.email),
|
|
251
|
+
|
|
252
|
+
// Composite index
|
|
253
|
+
index('users_org_name_idx').on(table.organizationId, table.name),
|
|
254
|
+
]);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Primary Key Constraints
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { pgTable, text, primaryKey } from 'drizzle-orm/pg-core';
|
|
261
|
+
|
|
262
|
+
// Composite primary key
|
|
263
|
+
export const userRoles = pgTable('user_roles', {
|
|
264
|
+
userId: text('user_id').notNull(),
|
|
265
|
+
roleId: text('role_id').notNull(),
|
|
266
|
+
}, (table) => [
|
|
267
|
+
primaryKey({ columns: [table.userId, table.roleId] })
|
|
268
|
+
]);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Unique Constraints
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { pgTable, text, unique } from 'drizzle-orm/pg-core';
|
|
275
|
+
|
|
276
|
+
export const profiles = pgTable('profiles', {
|
|
277
|
+
id: id(),
|
|
278
|
+
userId: text('user_id').notNull(),
|
|
279
|
+
type: text('type').notNull(),
|
|
280
|
+
...timestamps()
|
|
281
|
+
}, (table) => [
|
|
282
|
+
// Composite unique constraint
|
|
283
|
+
unique('profiles_user_type_unique').on(table.userId, table.type)
|
|
284
|
+
]);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Foreign Key Constraints
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { pgTable, text, foreignKey as fk } from 'drizzle-orm/pg-core';
|
|
291
|
+
|
|
292
|
+
export const posts = pgTable('posts', {
|
|
293
|
+
id: id(),
|
|
294
|
+
authorId: text('author_id').notNull(),
|
|
295
|
+
editorId: text('editor_id'),
|
|
296
|
+
...timestamps()
|
|
297
|
+
}, (table) => [
|
|
298
|
+
// Explicit foreign key with custom options
|
|
299
|
+
fk({
|
|
300
|
+
columns: [table.authorId],
|
|
301
|
+
foreignColumns: [users.id],
|
|
302
|
+
onDelete: 'cascade',
|
|
303
|
+
onUpdate: 'cascade'
|
|
304
|
+
}).name('posts_author_fk'),
|
|
305
|
+
|
|
306
|
+
fk({
|
|
307
|
+
columns: [table.editorId],
|
|
308
|
+
foreignColumns: [users.id],
|
|
309
|
+
onDelete: 'set null'
|
|
310
|
+
}).name('posts_editor_fk')
|
|
311
|
+
]);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Check Constraints
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { pgTable, integer, check } from 'drizzle-orm/pg-core';
|
|
318
|
+
import { sql } from 'drizzle-orm';
|
|
319
|
+
|
|
320
|
+
export const products = pgTable('products', {
|
|
321
|
+
id: id(),
|
|
322
|
+
price: integer('price').notNull(),
|
|
323
|
+
quantity: integer('quantity').notNull(),
|
|
324
|
+
...timestamps()
|
|
325
|
+
}, (table) => [
|
|
326
|
+
check('price_positive', sql`${table.price} > 0`),
|
|
327
|
+
check('quantity_non_negative', sql`${table.quantity} >= 0`)
|
|
328
|
+
]);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Partial Indexes
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { pgTable, text, boolean, index } from 'drizzle-orm/pg-core';
|
|
335
|
+
import { sql } from 'drizzle-orm';
|
|
336
|
+
|
|
337
|
+
export const users = pgTable('users', {
|
|
338
|
+
id: id(),
|
|
339
|
+
email: text('email').notNull(),
|
|
340
|
+
isActive: boolean('is_active').notNull().default(true),
|
|
341
|
+
...timestamps()
|
|
342
|
+
}, (table) => [
|
|
343
|
+
// Index only active users
|
|
344
|
+
index('users_active_email_idx')
|
|
345
|
+
.on(table.email)
|
|
346
|
+
.where(sql`${table.isActive} = true`)
|
|
347
|
+
]);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Expression Indexes
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { pgTable, text, index } from 'drizzle-orm/pg-core';
|
|
354
|
+
import { sql } from 'drizzle-orm';
|
|
355
|
+
|
|
356
|
+
export const users = pgTable('users', {
|
|
357
|
+
id: id(),
|
|
358
|
+
email: text('email').notNull(),
|
|
359
|
+
...timestamps()
|
|
360
|
+
}, (table) => [
|
|
361
|
+
// Case-insensitive email index
|
|
362
|
+
index('users_email_lower_idx').on(sql`lower(${table.email})`)
|
|
363
|
+
]);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Relations
|
|
369
|
+
|
|
370
|
+
### One-to-Many
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { relations } from 'drizzle-orm';
|
|
374
|
+
|
|
375
|
+
export const users = pgTable('users', {
|
|
376
|
+
id: id(),
|
|
377
|
+
name: text('name').notNull(),
|
|
378
|
+
...timestamps()
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
export const posts = pgTable('posts', {
|
|
382
|
+
id: id(),
|
|
383
|
+
title: text('title').notNull(),
|
|
384
|
+
authorId: foreignKey('author', () => users.id),
|
|
385
|
+
...timestamps()
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Define relations
|
|
389
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
390
|
+
posts: many(posts)
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
394
|
+
author: one(users, {
|
|
395
|
+
fields: [posts.authorId],
|
|
396
|
+
references: [users.id]
|
|
397
|
+
})
|
|
398
|
+
}));
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Many-to-Many
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
export const users = pgTable('users', {
|
|
405
|
+
id: id(),
|
|
406
|
+
name: text('name').notNull()
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export const roles = pgTable('roles', {
|
|
410
|
+
id: id(),
|
|
411
|
+
name: text('name').notNull()
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Junction table
|
|
415
|
+
export const userRoles = pgTable('user_roles', {
|
|
416
|
+
userId: foreignKey('user', () => users.id),
|
|
417
|
+
roleId: foreignKey('role', () => roles.id)
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
421
|
+
userRoles: many(userRoles)
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
export const rolesRelations = relations(roles, ({ many }) => ({
|
|
425
|
+
userRoles: many(userRoles)
|
|
426
|
+
}));
|
|
427
|
+
|
|
428
|
+
export const userRolesRelations = relations(userRoles, ({ one }) => ({
|
|
429
|
+
user: one(users, {
|
|
430
|
+
fields: [userRoles.userId],
|
|
431
|
+
references: [users.id]
|
|
432
|
+
}),
|
|
433
|
+
role: one(roles, {
|
|
434
|
+
fields: [userRoles.roleId],
|
|
435
|
+
references: [roles.id]
|
|
436
|
+
})
|
|
437
|
+
}));
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Schema Namespacing
|
|
443
|
+
|
|
444
|
+
For package-based schema isolation.
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { createSchema, packageNameToSchema } from '@spfn/core/db';
|
|
448
|
+
|
|
449
|
+
// Create namespaced schema
|
|
450
|
+
const schema = createSchema('@spfn/cms');
|
|
451
|
+
// Creates PostgreSQL schema: spfn_cms
|
|
452
|
+
|
|
453
|
+
export const labels = schema.table('labels', {
|
|
454
|
+
id: id(),
|
|
455
|
+
name: text('name').notNull(),
|
|
456
|
+
...timestamps()
|
|
457
|
+
});
|
|
458
|
+
// Creates table: spfn_cms.labels
|
|
459
|
+
|
|
460
|
+
// Utility functions
|
|
461
|
+
packageNameToSchema('@spfn/cms'); // 'spfn_cms'
|
|
462
|
+
packageNameToSchema('@company/auth'); // 'company_auth'
|
|
463
|
+
packageNameToSchema('spfn-storage'); // 'spfn_storage'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Entity Export Pattern
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// src/server/entities/index.ts
|
|
472
|
+
export * from './users';
|
|
473
|
+
export * from './posts';
|
|
474
|
+
export * from './categories';
|
|
475
|
+
|
|
476
|
+
// Re-export relations
|
|
477
|
+
export * from './relations';
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Migration
|
|
483
|
+
|
|
484
|
+
### Generate Migration
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
npx spfn db generate
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Apply Migration
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
npx spfn db migrate
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### View Database
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
pnpm drizzle-kit studio
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Best Practices
|
|
505
|
+
|
|
506
|
+
### Do
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// 1. Use column helpers for consistency
|
|
510
|
+
export const users = pgTable('users', {
|
|
511
|
+
id: id(),
|
|
512
|
+
...timestamps()
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// 2. Define types from schema
|
|
516
|
+
export type User = typeof users.$inferSelect;
|
|
517
|
+
export type NewUser = typeof users.$inferInsert;
|
|
518
|
+
|
|
519
|
+
// 3. Use enum helpers for type safety
|
|
520
|
+
status: enumText('status', ['active', 'inactive']).notNull()
|
|
521
|
+
|
|
522
|
+
// 4. Use foreignKey helpers
|
|
523
|
+
authorId: foreignKey('author', () => users.id)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Don't
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// 1. Don't put business logic in entity files
|
|
530
|
+
export const users = pgTable('users', { ... });
|
|
531
|
+
export function validateUser() { ... } // Bad - move to repository
|
|
532
|
+
|
|
533
|
+
// 2. Don't define raw columns when helpers exist
|
|
534
|
+
id: bigserial('id', { mode: 'bigint' }).primaryKey() // Use id() instead
|
|
535
|
+
createdAt: timestamp('created_at').defaultNow() // Use timestamps() instead
|
|
536
|
+
|
|
537
|
+
// 3. Don't use magic strings for enums
|
|
538
|
+
role: text('role') // Bad - use enumText for type safety
|
|
539
|
+
```
|