sonamu 0.10.7 → 0.10.9
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/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +5 -12
- package/dist/bin/cli.js +10 -253
- package/dist/database/base-model.d.ts +1 -0
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +14 -8
- package/dist/database/db.d.ts +2 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +13 -3
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +4 -11
- package/dist/database/transaction-context.d.ts +8 -3
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/transaction-context.js +12 -7
- package/dist/migration/code-generation.js +2 -2
- package/dist/naite/naite-reporter.d.ts +8 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +9 -4
- package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
- package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +3 -3
- package/src/api/decorators.ts +11 -22
- package/src/bin/cli.ts +12 -375
- package/src/database/__tests__/transaction-scope.test.ts +227 -0
- package/src/database/base-model.ts +31 -6
- package/src/database/db.ts +17 -2
- package/src/database/puri-wrapper.ts +5 -22
- package/src/database/transaction-context.ts +14 -6
- package/src/migration/code-generation.ts +1 -1
- package/src/naite/naite-reporter.ts +8 -3
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
- package/src/skills/AGENTS.md +0 -108
- package/src/skills/sonamu/SKILL.md +0 -75
- package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
- package/src/skills/sonamu-api/SKILL.md +0 -480
- package/src/skills/sonamu-auth/SKILL.md +0 -327
- package/src/skills/sonamu-auth/references/plugins.md +0 -310
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
- package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
- package/src/skills/sonamu-config/SKILL.md +0 -203
- package/src/skills/sonamu-config/references/database.md +0 -452
- package/src/skills/sonamu-config/references/environments.md +0 -178
- package/src/skills/sonamu-config/references/server-options.md +0 -400
- package/src/skills/sonamu-entity/SKILL.md +0 -180
- package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
- package/src/skills/sonamu-entity/references/design-guides.md +0 -243
- package/src/skills/sonamu-entity/references/field-types.md +0 -170
- package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
- package/src/skills/sonamu-entity/references/relations.md +0 -463
- package/src/skills/sonamu-entity/references/subset.md +0 -156
- package/src/skills/sonamu-fixture/SKILL.md +0 -180
- package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
- package/src/skills/sonamu-fixture/references/cone.md +0 -298
- package/src/skills/sonamu-frontend/SKILL.md +0 -142
- package/src/skills/sonamu-frontend/references/components.md +0 -323
- package/src/skills/sonamu-frontend/references/examples.md +0 -64
- package/src/skills/sonamu-frontend/references/hooks.md +0 -273
- package/src/skills/sonamu-frontend/references/runtime.md +0 -165
- package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
- package/src/skills/sonamu-i18n/SKILL.md +0 -287
- package/src/skills/sonamu-migration/SKILL.md +0 -316
- package/src/skills/sonamu-naite/SKILL.md +0 -266
- package/src/skills/sonamu-query/SKILL.md +0 -48
- package/src/skills/sonamu-query/references/model-patterns.md +0 -390
- package/src/skills/sonamu-query/references/model.md +0 -366
- package/src/skills/sonamu-query/references/puri.md +0 -413
- package/src/skills/sonamu-query/references/search.md +0 -238
- package/src/skills/sonamu-query/references/upsert.md +0 -324
- package/src/skills/sonamu-tasks/SKILL.md +0 -236
- package/src/skills/sonamu-testing/SKILL.md +0 -251
- package/src/skills/sonamu-testing/references/devrunner.md +0 -405
- package/src/skills/sonamu-testing/references/helpers.md +0 -185
- package/src/skills/sonamu-testing/references/patterns.md +0 -263
- package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
- package/src/skills/sonamu-testing/references/quick-start.md +0 -285
- package/src/skills/sonamu-testing/references/type-safety.md +0 -172
- package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
- package/src/skills/sonamu-vector/SKILL.md +0 -222
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-i18n
|
|
3
|
-
description: Adds and resolves translation keys through Sonamu's SD function. Use when registering labels for a new entity or enum, adding a language, or a UI string renders as a raw key. Covers dictionary configuration, SD(), and the i18n helper functions.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# i18n (Internationalization)
|
|
7
|
-
|
|
8
|
-
## Configuration
|
|
9
|
-
|
|
10
|
-
`sonamu.config.ts`:
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
export default defineConfig({
|
|
14
|
-
i18n: {
|
|
15
|
-
defaultLocale: "ko",
|
|
16
|
-
supportedLocales: ["ko", "en"],
|
|
17
|
-
},
|
|
18
|
-
// ...
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## File Structure
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
packages/api/src/i18n/
|
|
26
|
-
├── ko.ts # Default locale (defaultLocale)
|
|
27
|
-
├── en.ts # Additional locale
|
|
28
|
-
└── sd.generated.ts # Auto-generated (pnpm sync)
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Dictionary Files
|
|
32
|
-
|
|
33
|
-
### Default locale (ko.ts)
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { createFormat, josa } from "sonamu/dict";
|
|
37
|
-
|
|
38
|
-
const format = createFormat("ko");
|
|
39
|
-
|
|
40
|
-
export default {
|
|
41
|
-
"common.save": "저장",
|
|
42
|
-
"common.cancel": "취소",
|
|
43
|
-
"validation.required": (field: string) => `${josa(field, "은는")} 필수입니다`,
|
|
44
|
-
"common.results": (count: number) => `${count}개 결과`,
|
|
45
|
-
test: (date: Date) => format.date(date),
|
|
46
|
-
} as const;
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Additional locale (en.ts)
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import { plural } from "sonamu/dict";
|
|
53
|
-
import { defineLocale } from "./sd.generated";
|
|
54
|
-
|
|
55
|
-
export default defineLocale({
|
|
56
|
-
"common.save": "Save",
|
|
57
|
-
"common.cancel": "Cancel",
|
|
58
|
-
"validation.required": (field: string) => `${field} is required`,
|
|
59
|
-
"common.results": (count: number) =>
|
|
60
|
-
plural(count, { one: `${count} result`, other: `${count} results` }),
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Using the SD Function
|
|
65
|
-
|
|
66
|
-
### Basic Usage
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { SD } from "@/i18n/sd.generated";
|
|
70
|
-
|
|
71
|
-
// Simple string
|
|
72
|
-
SD("common.save"); // → "저장" (ko) / "Save" (en)
|
|
73
|
-
|
|
74
|
-
// Function form (with parameters)
|
|
75
|
-
SD("validation.required")("이메일"); // → "이메일은 필수입니다"
|
|
76
|
-
SD("common.results")(5); // → "5개 결과" (ko) / "5 results" (en)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Force a Specific Locale
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
const EN = SD.locale("en");
|
|
83
|
-
EN("common.save"); // → "Save" (always English)
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Enum Labels
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// Enum labels defined in entity.json
|
|
90
|
-
SD.enumLabels("ProjectStatus")["in_progress"]; // → "진행중"
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Helper Functions
|
|
94
|
-
|
|
95
|
-
### plural (pluralization)
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import { plural } from "sonamu/dict";
|
|
99
|
-
|
|
100
|
-
// Handle singular/plural in English
|
|
101
|
-
plural(count, {
|
|
102
|
-
zero: "No items", // count === 0
|
|
103
|
-
one: "1 item", // count === 1
|
|
104
|
-
other: `${count} items`, // everything else
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### josa (Korean postposition helper)
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
import { josa } from "sonamu/dict";
|
|
112
|
-
|
|
113
|
-
josa("이메일", "은는"); // → "이메일은"
|
|
114
|
-
josa("이름", "이가"); // → "이름이"
|
|
115
|
-
josa("파일", "을를"); // → "파일을"
|
|
116
|
-
josa("회사", "과와"); // → "회사와"
|
|
117
|
-
josa("서울", "으로"); // → "서울로"
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### createFormat (number/date formatting)
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import { createFormat } from "sonamu/dict";
|
|
124
|
-
|
|
125
|
-
const format = createFormat("ko");
|
|
126
|
-
format.number(1234567); // → "1,234,567"
|
|
127
|
-
format.date(new Date()); // → "2024. 1. 15."
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## Entity Integration
|
|
131
|
-
|
|
132
|
-
Entity title, prop descriptions, and enum labels are automatically included in the dictionary.
|
|
133
|
-
|
|
134
|
-
```
|
|
135
|
-
entity.{EntityId} → Entity title
|
|
136
|
-
entity.{EntityId}.{propName} → Property description
|
|
137
|
-
enum.{EnumId}.{value} → Enum value label
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Example:
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
SD("entity.User"); // → "사용자"
|
|
144
|
-
SD("entity.User.email"); // → "이메일"
|
|
145
|
-
SD("enum.UserRole.admin"); // → "관리자"
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## localizedColumn
|
|
149
|
-
|
|
150
|
-
Returns the value for the current locale when locale-specific columns or locale maps exist in the DB:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
import { localizedColumn } from "@/i18n/sd.generated";
|
|
154
|
-
|
|
155
|
-
// DB: { name: "태그", name_ko: "태그", name_en: "Tag" }
|
|
156
|
-
localizedColumn(tag, "name"); // → "태그" (ko) / "Tag" (en)
|
|
157
|
-
|
|
158
|
-
// DB: { name: { ko: ["태그"], en: ["Tag"] } }
|
|
159
|
-
localizedColumn(tag, "name"); // → ["태그"] (ko) / ["Tag"] (en)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
- `string[]` values are returned as arrays instead of being stringified.
|
|
163
|
-
- Unsupported Context locales use `defaultLocale` before lookup.
|
|
164
|
-
- Direct suffix/base scalar values such as numbers are stringified for compatibility.
|
|
165
|
-
- Locale map values support only `string` and `string[]`.
|
|
166
|
-
|
|
167
|
-
## Sonamu UI Management
|
|
168
|
-
|
|
169
|
-
`http://localhost:34900/sonamu-ui` → i18n tab:
|
|
170
|
-
|
|
171
|
-
- Browse/edit dictionary
|
|
172
|
-
- Add/delete keys
|
|
173
|
-
- Excel import/export
|
|
174
|
-
- Detect unused keys
|
|
175
|
-
|
|
176
|
-
## Key Naming Conventions
|
|
177
|
-
|
|
178
|
-
| Pattern | Purpose | Example |
|
|
179
|
-
| -------------- | ------------------------- | ----------------------------------------- |
|
|
180
|
-
| `common.*` | Common UI text | `common.save`, `common.cancel` |
|
|
181
|
-
| `error.*` | Error messages | `error.notFound`, `error.forbidden` |
|
|
182
|
-
| `validation.*` | Validation messages | `validation.required`, `validation.email` |
|
|
183
|
-
| `entity.*` | Entity-related | `entity.User`, `entity.User.email` |
|
|
184
|
-
| `enum.*` | Enum labels | `enum.UserRole.admin` |
|
|
185
|
-
| `menu.*` | Menu/navigation | `menu.home`, `menu.settings` |
|
|
186
|
-
| `rc.*` | react-components built-in | `rc.pagination.next` |
|
|
187
|
-
|
|
188
|
-
## Troubleshooting
|
|
189
|
-
|
|
190
|
-
### LocalizedString Type Error
|
|
191
|
-
|
|
192
|
-
When you see `Type 'string' is not assignable to type '{ __brand: "LocalizedString"; }'`:
|
|
193
|
-
|
|
194
|
-
**Cause 1: Missing i18n key in ko.ts**
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// Wrong: type error when key is missing from ko.ts
|
|
198
|
-
SD("validation.password"); // key not in ko.ts
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Fix: Add the key to ko.ts and run `pnpm sync`
|
|
202
|
-
|
|
203
|
-
**Cause 2: Not using SD() in the model**
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// Wrong: using a raw string directly
|
|
207
|
-
throw new BadRequestException("이메일이 잘못되었습니다");
|
|
208
|
-
|
|
209
|
-
// Correct: use the SD() function
|
|
210
|
-
throw new BadRequestException(SD("error.invalidEmail"));
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
All user-facing strings must be wrapped with the SD() function.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## Type Safety
|
|
218
|
-
|
|
219
|
-
### String PK Support
|
|
220
|
-
|
|
221
|
-
When an Entity uses a String PK, the i18n function types must reflect this.
|
|
222
|
-
|
|
223
|
-
**BAD: Number only**
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
// ko.ts
|
|
227
|
-
notFound: (name: string, id: number) => `존재하지 않는 ${name} ID ${id}`,
|
|
228
|
-
|
|
229
|
-
// Type error when used with String PK entities
|
|
230
|
-
// Account, Session, User, Verification, etc.
|
|
231
|
-
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
232
|
-
// Error: Argument of type 'string' is not assignable to parameter of type 'number'
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**GOOD: Support both with a union type**
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
// ko.ts
|
|
239
|
-
notFound: (name: string, id: number | string) => `존재하지 않는 ${name} ID ${id}`,
|
|
240
|
-
|
|
241
|
-
// Works for both Number PK and String PK
|
|
242
|
-
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
**Applicable i18n keys:**
|
|
246
|
-
|
|
247
|
-
- `notFound`: Entity lookup failure
|
|
248
|
-
- `error.entityNotFound`: Specific entity lookup failure (deprecated, prefer `notFound`)
|
|
249
|
-
|
|
250
|
-
### Type Patterns by Namespace
|
|
251
|
-
|
|
252
|
-
| i18n key | Parameter type | Purpose |
|
|
253
|
-
| ---------------------- | -------------------------------------- | ------------------------- |
|
|
254
|
-
| `notFound` | `(name: string, id: number \| string)` | Entity lookup failure |
|
|
255
|
-
| `search.invalidField` | `(field: string)` | Invalid search field |
|
|
256
|
-
| `validation.required` | `(field: string)` | Required field validation |
|
|
257
|
-
| `validation.maxLength` | `(field: string, max: number)` | Max length validation |
|
|
258
|
-
| `validation.minLength` | `(field: string, min: number)` | Min length validation |
|
|
259
|
-
| `entity.create` | `(name: string)` | Entity creation screen |
|
|
260
|
-
| `entity.edit` | `(name: string, id: number \| string)` | Entity edit screen |
|
|
261
|
-
|
|
262
|
-
### How to Check PK Type
|
|
263
|
-
|
|
264
|
-
Check the id field type in the Entity file:
|
|
265
|
-
|
|
266
|
-
```json
|
|
267
|
-
// String PK
|
|
268
|
-
{
|
|
269
|
-
"props": {
|
|
270
|
-
"id": { "type": "varchar(255)", "autoIncrement": false }
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Number PK (default)
|
|
275
|
-
{
|
|
276
|
-
"props": {
|
|
277
|
-
"id": { "type": "int", "autoIncrement": true }
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**Entities that use a string PK (generated by better-auth):**
|
|
283
|
-
|
|
284
|
-
- Account
|
|
285
|
-
- Session
|
|
286
|
-
- User
|
|
287
|
-
- Verification
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-migration
|
|
3
|
-
description: Generates and applies Sonamu database migrations. Use when a schema change needs a migration, a migration fails or conflicts, FK ordering breaks an apply, or a primary key type must change. Covers sonamu migrate generate/run/apply/status, up and down functions, and CREATE/ALTER TABLE output.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Migration
|
|
7
|
-
|
|
8
|
-
## CRITICAL: Migrations are generated via Sonamu UI or CLI
|
|
9
|
-
|
|
10
|
-
**Do not write migration files manually or execute SQL directly.** Sonamu detects entity.json changes and generates accurate migration files.
|
|
11
|
-
|
|
12
|
-
**Prerequisites:**
|
|
13
|
-
|
|
14
|
-
- `pnpm dev` must be running in `/packages/api`
|
|
15
|
-
|
|
16
|
-
**Method 1: Sonamu UI (confirm with user before choosing)**
|
|
17
|
-
|
|
18
|
-
1. Open Sonamu UI in browser: `http://localhost:34900/sonamu-ui`
|
|
19
|
-
2. Check the prepared list in the Migration menu
|
|
20
|
-
3. Generate a migration file with the "Generate" button
|
|
21
|
-
4. Apply to the actual DB with the "Apply" button
|
|
22
|
-
|
|
23
|
-
**Method 2: CLI**
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
cd packages/api
|
|
27
|
-
pnpm sonamu migrate generate # Generate migration file
|
|
28
|
-
pnpm sonamu migrate run # Apply to actual DB
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**CRITICAL: Ask the user whether to proceed with UI or CLI before starting.**
|
|
32
|
-
|
|
33
|
-
**DO NOT:**
|
|
34
|
-
|
|
35
|
-
- Write migration files manually
|
|
36
|
-
- Execute SQL directly (`CREATE TABLE`, `ALTER TABLE`, etc.)
|
|
37
|
-
|
|
38
|
-
**Exception:** Only raw SQL is allowed for special cases that Sonamu cannot handle automatically, such as PK type changes (see "PK Type Change" section below)
|
|
39
|
-
|
|
40
|
-
**CRITICAL: Handle cross-table cascading changes in a single file.** Cascading changes across multiple tables — such as FK drop → type change → FK restore — must be executed in order within a single migration file. Splitting into separate files causes constraint violations in intermediate states. (Details: see "PK Type Change" section below)
|
|
41
|
-
|
|
42
|
-
## Basic Structure
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import type { Knex } from "knex";
|
|
46
|
-
|
|
47
|
-
export async function up(knex: Knex): Promise<void> {
|
|
48
|
-
// Apply changes
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function down(knex: Knex): Promise<void> {
|
|
52
|
-
// Rollback changes
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## CREATE TABLE
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
export async function up(knex: Knex): Promise<void> {
|
|
60
|
-
await knex.schema.createTable("users", (table) => {
|
|
61
|
-
table.increments().primary();
|
|
62
|
-
table.string("email", 255).notNullable();
|
|
63
|
-
table.string("username", 255).notNullable();
|
|
64
|
-
table.text("role").notNullable();
|
|
65
|
-
table.timestamp("created_at", { useTz: true }).defaultTo(knex.fn.now());
|
|
66
|
-
table.unique(["email"], "users_email_unique");
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function down(knex: Knex): Promise<void> {
|
|
71
|
-
return knex.schema.dropTable("users");
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## ALTER TABLE
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
// Add column
|
|
79
|
-
export async function up(knex: Knex): Promise<void> {
|
|
80
|
-
await knex.schema.alterTable("users", (table) => {
|
|
81
|
-
table.string("phone", 20).nullable();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Drop column
|
|
86
|
-
export async function down(knex: Knex): Promise<void> {
|
|
87
|
-
await knex.schema.alterTable("users", (table) => {
|
|
88
|
-
table.dropColumns("phone");
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## FOREIGN KEY
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
export async function up(knex: Knex): Promise<void> {
|
|
97
|
-
return knex.schema.alterTable("employees", (table) => {
|
|
98
|
-
table.foreign("user_id").references("users.id").onUpdate("CASCADE").onDelete("CASCADE");
|
|
99
|
-
table
|
|
100
|
-
.foreign("department_id")
|
|
101
|
-
.references("departments.id")
|
|
102
|
-
.onUpdate("CASCADE")
|
|
103
|
-
.onDelete("SET NULL");
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function down(knex: Knex): Promise<void> {
|
|
108
|
-
return knex.schema.alterTable("employees", (table) => {
|
|
109
|
-
table.dropForeign(["user_id"]);
|
|
110
|
-
table.dropForeign(["department_id"]);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## PK Type Change
|
|
116
|
-
|
|
117
|
-
### Situation
|
|
118
|
-
|
|
119
|
-
When the PK type of an existing table must be changed (e.g. integer -> text, bigint -> uuid), with FKs referencing that PK in multiple tables.
|
|
120
|
-
|
|
121
|
-
### Required Order
|
|
122
|
-
|
|
123
|
-
When changing the type of a column that has referential integrity (a PK referenced by FKs), the following order must be followed:
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
export async function up(knex: Knex): Promise<void> {
|
|
127
|
-
// Step 1: DROP all FK constraints referencing this PK
|
|
128
|
-
await knex.raw('ALTER TABLE "child_table_1" DROP CONSTRAINT "child_table_1_parent_id_foreign"');
|
|
129
|
-
await knex.raw('ALTER TABLE "child_table_2" DROP CONSTRAINT "child_table_2_parent_id_foreign"');
|
|
130
|
-
|
|
131
|
-
// Step 2: DROP the PK constraint
|
|
132
|
-
await knex.raw('ALTER TABLE "parent_table" DROP CONSTRAINT "parent_table_pkey"');
|
|
133
|
-
|
|
134
|
-
// Step 3: Change types of the PK column and all FK columns simultaneously
|
|
135
|
-
await knex.raw('ALTER TABLE "parent_table" ALTER COLUMN "id" TYPE new_type USING "id"::new_type');
|
|
136
|
-
await knex.raw(
|
|
137
|
-
'ALTER TABLE "child_table_1" ALTER COLUMN "parent_id" TYPE new_type USING "parent_id"::new_type',
|
|
138
|
-
);
|
|
139
|
-
await knex.raw(
|
|
140
|
-
'ALTER TABLE "child_table_2" ALTER COLUMN "parent_id" TYPE new_type USING "parent_id"::new_type',
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Step 4: ADD the PK constraint
|
|
144
|
-
await knex.raw(
|
|
145
|
-
'ALTER TABLE "parent_table" ADD CONSTRAINT "parent_table_pkey" PRIMARY KEY ("id")',
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Step 5: ADD all FK constraints
|
|
149
|
-
await knex.raw(
|
|
150
|
-
'ALTER TABLE "child_table_1" ADD CONSTRAINT "child_table_1_parent_id_foreign" FOREIGN KEY ("parent_id") REFERENCES "parent_table"("id") ON UPDATE RESTRICT ON DELETE CASCADE',
|
|
151
|
-
);
|
|
152
|
-
await knex.raw(
|
|
153
|
-
'ALTER TABLE "child_table_2" ADD CONSTRAINT "child_table_2_parent_id_foreign" FOREIGN KEY ("parent_id") REFERENCES "parent_table"("id") ON UPDATE RESTRICT ON DELETE RESTRICT',
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Core Principles
|
|
159
|
-
|
|
160
|
-
1. **Cannot change the type of a referenced column while FK constraints exist**: PostgreSQL throws an error if the types of the FK and referenced PK do not match. Constraints must be removed first.
|
|
161
|
-
|
|
162
|
-
2. **Handle all changes in a single migration**: Splitting into multiple migrations causes constraint violations in intermediate states. Change the types of the PK and all FKs in one go.
|
|
163
|
-
|
|
164
|
-
3. **Prefer raw SQL over the knex schema builder**: Guarantees clear execution order, allows explicit constraint naming, and supports complex type conversions (USING).
|
|
165
|
-
|
|
166
|
-
### Real Example: users.id integer -> text
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
export async function up(knex: Knex): Promise<void> {
|
|
170
|
-
// 1. Remove FK constraints
|
|
171
|
-
await knex.raw('ALTER TABLE "accounts" DROP CONSTRAINT "accounts_user_id_foreign"');
|
|
172
|
-
await knex.raw('ALTER TABLE "sessions" DROP CONSTRAINT "sessions_user_id_foreign"');
|
|
173
|
-
|
|
174
|
-
// 2. Remove PK constraint
|
|
175
|
-
await knex.raw('ALTER TABLE "users" DROP CONSTRAINT "users_pkey"');
|
|
176
|
-
|
|
177
|
-
// 3. Change types (explicit conversion with USING clause)
|
|
178
|
-
await knex.raw('ALTER TABLE "users" ALTER COLUMN "id" TYPE text USING "id"::text');
|
|
179
|
-
await knex.raw('ALTER TABLE "accounts" ALTER COLUMN "user_id" TYPE text USING "user_id"::text');
|
|
180
|
-
await knex.raw('ALTER TABLE "sessions" ALTER COLUMN "user_id" TYPE text USING "user_id"::text');
|
|
181
|
-
|
|
182
|
-
// 4. Restore PK constraint
|
|
183
|
-
await knex.raw('ALTER TABLE "users" ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id")');
|
|
184
|
-
|
|
185
|
-
// 5. Restore FK constraints
|
|
186
|
-
await knex.raw(
|
|
187
|
-
'ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_foreign" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE RESTRICT ON DELETE CASCADE',
|
|
188
|
-
);
|
|
189
|
-
await knex.raw(
|
|
190
|
-
'ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_foreign" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE RESTRICT ON DELETE CASCADE',
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Finding FK Reference Tables
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
# Find relations referencing a specific entity in entity files
|
|
199
|
-
grep -r "with.*User" --include="*.entity.json"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Common Mistakes
|
|
203
|
-
|
|
204
|
-
1. **Splitting into multiple migrations**: Separating FK drop, type change, and FK restore into separate files causes a constraint violation immediately after the first file is applied. Always consolidate related changes into a single file.
|
|
205
|
-
|
|
206
|
-
2. **Changing type without removing constraint**: Causes `cannot alter type of a column used by a foreign key` error
|
|
207
|
-
|
|
208
|
-
3. **Missing USING clause**: Causes `column "id" cannot be cast automatically to type text` error. `USING "id"::text` is required for integer -> text.
|
|
209
|
-
|
|
210
|
-
4. **Constraint name mismatch**: Verify the exact constraint name with `SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = 'accounts'`.
|
|
211
|
-
|
|
212
|
-
## Commands
|
|
213
|
-
|
|
214
|
-
Run from the **`packages/api`** directory:
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
cd packages/api
|
|
218
|
-
pnpm sonamu migrate run # Run all pending migrations on local DB
|
|
219
|
-
pnpm sonamu migrate status # Check migration status
|
|
220
|
-
pnpm sonamu migrate apply # Apply to a specific DB configuration target
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Note**: `migrate up` and `migrate rollback` are not provided in the CLI. Rollback is available from Sonamu UI.
|
|
224
|
-
|
|
225
|
-
## Entity Type → DB Type
|
|
226
|
-
|
|
227
|
-
| Entity | DB | Knex |
|
|
228
|
-
| -------------- | ------------------------- | ------------------------------------------- |
|
|
229
|
-
| `string` | `varchar(n)` / `text` | `table.string(name, length)` |
|
|
230
|
-
| `string[]` | `varchar(n)[]` / `text[]` | `table.specificType(name, 'text[]')` |
|
|
231
|
-
| `integer` | `integer` | `table.integer(name)` |
|
|
232
|
-
| `integer[]` | `integer[]` | `table.specificType(name, 'integer[]')` |
|
|
233
|
-
| `bigInteger` | `bigint` | `table.bigInteger(name)` |
|
|
234
|
-
| `bigInteger[]` | `bigint[]` | `table.specificType(name, 'bigint[]')` |
|
|
235
|
-
| `boolean` | `boolean` | `table.boolean(name)` |
|
|
236
|
-
| `boolean[]` | `boolean[]` | `table.specificType(name, 'boolean[]')` |
|
|
237
|
-
| `number` | `numeric(p,s)` | `table.decimal(name, p, s)` |
|
|
238
|
-
| `numeric` | `numeric(p,s)` | `table.decimal(name, p, s)` |
|
|
239
|
-
| `date` | `timestamptz` | `table.timestamp(name, { useTz: true })` |
|
|
240
|
-
| `date[]` | `timestamptz[]` | `table.specificType(name, 'timestamptz[]')` |
|
|
241
|
-
| `uuid` | `uuid` | `table.uuid(name)` |
|
|
242
|
-
| `uuid[]` | `uuid[]` | `table.specificType(name, 'uuid[]')` |
|
|
243
|
-
| `json` | `jsonb` | `table.jsonb(name)` |
|
|
244
|
-
| `enum` | `text` | `table.text(name)` |
|
|
245
|
-
| `vector` | `vector(n)` | `table.specificType(name, 'vector(n)')` |
|
|
246
|
-
| `vector[]` | `vector(n)[]` | `table.specificType(name, 'vector(n)[]')` |
|
|
247
|
-
| `tsvector` | `tsvector` | `table.specificType(name, 'tsvector')` |
|
|
248
|
-
|
|
249
|
-
## Execution Order (Important!)
|
|
250
|
-
|
|
251
|
-
```
|
|
252
|
-
1. CREATE TABLE companies (no dependencies)
|
|
253
|
-
2. CREATE TABLE departments (company_id column only)
|
|
254
|
-
3. CREATE TABLE users (no dependencies)
|
|
255
|
-
4. CREATE TABLE employees (user_id, department_id columns only)
|
|
256
|
-
5. FOREIGN KEY departments (company_id → companies.id)
|
|
257
|
-
6. FOREIGN KEY employees (user_id → users.id, etc.)
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
## Rules
|
|
261
|
-
|
|
262
|
-
- Migration files are generated from Sonamu UI
|
|
263
|
-
- MUST implement rollback logic in `down` function
|
|
264
|
-
- Foreign keys MUST be added in separate migration after table creation
|
|
265
|
-
|
|
266
|
-
## IMPORTANT: Check Before Running sync
|
|
267
|
-
|
|
268
|
-
### 1. MUST Build dist First
|
|
269
|
-
|
|
270
|
-
The sync command references `dist/sonamu.config.js`. An error will occur if it does not exist.
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
cd packages/api
|
|
274
|
-
pnpm exec tsdown --config tsdown.config.ts
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### 2. Verify DB Connection
|
|
278
|
-
|
|
279
|
-
```bash
|
|
280
|
-
# Check Docker containers
|
|
281
|
-
docker ps | grep container
|
|
282
|
-
|
|
283
|
-
# Check DB configuration in sonamu.config.ts
|
|
284
|
-
# - host, port, database, user, password
|
|
285
|
-
# - Distinguish PostgreSQL vs MySQL
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### 3. Circular References
|
|
289
|
-
|
|
290
|
-
Split the migration, or set the FK to nullable and add it later.
|
|
291
|
-
|
|
292
|
-
## Full Workflow
|
|
293
|
-
|
|
294
|
-
```bash
|
|
295
|
-
cd packages/api
|
|
296
|
-
|
|
297
|
-
# 1. Build config
|
|
298
|
-
pnpm exec tsdown --config tsdown.config.ts
|
|
299
|
-
|
|
300
|
-
# 2. Run sync
|
|
301
|
-
pnpm sonamu sync
|
|
302
|
-
|
|
303
|
-
# 3. Generate/run migration from Sonamu UI
|
|
304
|
-
|
|
305
|
-
# 4. Run scaffolding
|
|
306
|
-
|
|
307
|
-
# 5. Check/add orderBy cases in model (see model.md)
|
|
308
|
-
|
|
309
|
-
# 6. Build API
|
|
310
|
-
npm run build
|
|
311
|
-
|
|
312
|
-
# 7. Check/fix web form (see frontend.md)
|
|
313
|
-
|
|
314
|
-
# 8. Build web
|
|
315
|
-
cd ../web && npm run build
|
|
316
|
-
```
|