@venizia/ignis-docs 0.0.1-9 → 0.0.2
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/LICENSE.md +1 -0
- package/package.json +2 -2
- package/wiki/changelogs/{v0.0.1-7-initial-architecture.md → 2025-12-16-initial-architecture.md} +20 -12
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
- package/wiki/changelogs/2025-12-17-refactor.md +80 -12
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +28 -90
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +101 -297
- package/wiki/changelogs/index.md +20 -8
- package/wiki/changelogs/planned-schema-migrator.md +561 -0
- package/wiki/changelogs/planned-transaction-support.md +216 -0
- package/wiki/changelogs/template.md +123 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -2
- package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
- package/wiki/get-started/best-practices/code-style-standards.md +575 -10
- package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
- package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
- package/wiki/get-started/best-practices/data-modeling.md +91 -34
- package/wiki/get-started/best-practices/security-guidelines.md +3 -1
- package/wiki/get-started/building-a-crud-api.md +3 -3
- package/wiki/get-started/core-concepts/application.md +72 -3
- package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
- package/wiki/get-started/core-concepts/components.md +4 -2
- package/wiki/get-started/core-concepts/persistent.md +350 -378
- package/wiki/get-started/core-concepts/services.md +21 -27
- package/wiki/references/base/bootstrapping.md +789 -0
- package/wiki/references/base/components.md +1 -1
- package/wiki/references/base/dependency-injection.md +95 -2
- package/wiki/references/base/services.md +2 -2
- package/wiki/references/components/authentication.md +4 -3
- package/wiki/references/components/index.md +1 -1
- package/wiki/references/helpers/error.md +2 -2
- package/wiki/references/src-details/boot.md +379 -0
- package/wiki/references/src-details/core.md +2 -2
- package/wiki/changelogs/v0.0.1-8-model-repo-datasource-refactor.md +0 -278
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Planned - Schema Migrator (Auto-Update)
|
|
3
|
+
description: Implementation plan for LoopBack 4-style auto schema migration without Drizzle Kit
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Planned: Schema Migrator (Auto-Update)
|
|
7
|
+
|
|
8
|
+
**Status:** Planned (Not Yet Implemented)
|
|
9
|
+
**Priority:** High
|
|
10
|
+
|
|
11
|
+
## Goal
|
|
12
|
+
|
|
13
|
+
Implement LoopBack 4-style automatic schema migration that reads model definitions from `@model` decorated classes and syncs them to the database. No external CLI tools (like Drizzle Kit) required.
|
|
14
|
+
|
|
15
|
+
Key principle: **Never drop tables or lose data** - only apply incremental changes (ADD/ALTER/DROP columns).
|
|
16
|
+
|
|
17
|
+
## Target API
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// In application boot
|
|
21
|
+
const migrator = dataSource.getMigrator();
|
|
22
|
+
|
|
23
|
+
// Auto-update: safe, only applies differences
|
|
24
|
+
await migrator.autoupdate();
|
|
25
|
+
|
|
26
|
+
// Or with options
|
|
27
|
+
await migrator.autoupdate({
|
|
28
|
+
models: [User, Role], // Specific models only (optional)
|
|
29
|
+
dryRun: true, // Preview SQL without executing
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Fresh start (destructive - use with caution)
|
|
33
|
+
await migrator.automigrate(); // Drops and recreates all tables
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration Behaviors
|
|
37
|
+
|
|
38
|
+
| Method | Behavior | Data Loss |
|
|
39
|
+
|--------|----------|-----------|
|
|
40
|
+
| `autoupdate()` | Compares DB ↔ Model, applies ALTER statements | **No** |
|
|
41
|
+
| `automigrate()` | Drops and recreates tables | **Yes** |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Implementation Steps
|
|
46
|
+
|
|
47
|
+
### Step 1: Define Migrator Types
|
|
48
|
+
|
|
49
|
+
**File:** `packages/core/src/base/datasources/types.ts`
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
/** Column information from database introspection */
|
|
53
|
+
export interface IColumnInfo {
|
|
54
|
+
column_name: string;
|
|
55
|
+
data_type: string;
|
|
56
|
+
is_nullable: 'YES' | 'NO';
|
|
57
|
+
column_default: string | null;
|
|
58
|
+
character_maximum_length: number | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Constraint information from database */
|
|
62
|
+
export interface IConstraintInfo {
|
|
63
|
+
constraint_name: string;
|
|
64
|
+
constraint_type: 'PRIMARY KEY' | 'FOREIGN KEY' | 'UNIQUE' | 'CHECK';
|
|
65
|
+
column_name: string;
|
|
66
|
+
foreign_table_name?: string;
|
|
67
|
+
foreign_column_name?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Index information from database */
|
|
71
|
+
export interface IIndexInfo {
|
|
72
|
+
index_name: string;
|
|
73
|
+
column_name: string;
|
|
74
|
+
is_unique: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Options for autoupdate */
|
|
78
|
+
export interface IAutoupdateOptions {
|
|
79
|
+
/** Only migrate specific models (default: all registered models) */
|
|
80
|
+
models?: Array<typeof BaseEntity>;
|
|
81
|
+
/** Preview SQL without executing (default: false) */
|
|
82
|
+
dryRun?: boolean;
|
|
83
|
+
/** Log generated SQL statements (default: true) */
|
|
84
|
+
verbose?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Options for automigrate */
|
|
88
|
+
export interface IAutomigrateOptions extends IAutoupdateOptions {
|
|
89
|
+
/** Skip confirmation for destructive operation (default: false) */
|
|
90
|
+
force?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Result of migration operation */
|
|
94
|
+
export interface IMigrationResult {
|
|
95
|
+
/** Tables created */
|
|
96
|
+
created: string[];
|
|
97
|
+
/** Tables altered */
|
|
98
|
+
altered: string[];
|
|
99
|
+
/** SQL statements executed (or would be executed if dryRun) */
|
|
100
|
+
statements: string[];
|
|
101
|
+
/** Errors encountered */
|
|
102
|
+
errors: Array<{ table: string; error: string }>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 2: Create Schema Introspector
|
|
107
|
+
|
|
108
|
+
**File:** `packages/core/src/base/datasources/introspector.ts`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { sql } from 'drizzle-orm';
|
|
112
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Introspects PostgreSQL database schema using information_schema
|
|
116
|
+
*/
|
|
117
|
+
export class SchemaIntrospector {
|
|
118
|
+
constructor(private db: NodePgDatabase) {}
|
|
119
|
+
|
|
120
|
+
/** Check if table exists */
|
|
121
|
+
async tableExists(tableName: string): Promise<boolean>;
|
|
122
|
+
|
|
123
|
+
/** Get all columns for a table */
|
|
124
|
+
async getColumns(tableName: string): Promise<Map<string, IColumnInfo>>;
|
|
125
|
+
|
|
126
|
+
/** Get all constraints for a table */
|
|
127
|
+
async getConstraints(tableName: string): Promise<IConstraintInfo[]>;
|
|
128
|
+
|
|
129
|
+
/** Get all indexes for a table */
|
|
130
|
+
async getIndexes(tableName: string): Promise<IIndexInfo[]>;
|
|
131
|
+
|
|
132
|
+
/** Get all table names in public schema */
|
|
133
|
+
async getAllTables(): Promise<string[]>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Key Queries:**
|
|
138
|
+
|
|
139
|
+
```sql
|
|
140
|
+
-- Get columns
|
|
141
|
+
SELECT column_name, data_type, is_nullable, column_default, character_maximum_length
|
|
142
|
+
FROM information_schema.columns
|
|
143
|
+
WHERE table_schema = 'public' AND table_name = $1;
|
|
144
|
+
|
|
145
|
+
-- Get constraints
|
|
146
|
+
SELECT tc.constraint_name, tc.constraint_type, kcu.column_name,
|
|
147
|
+
ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
|
|
148
|
+
FROM information_schema.table_constraints tc
|
|
149
|
+
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
|
|
150
|
+
LEFT JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
|
|
151
|
+
WHERE tc.table_schema = 'public' AND tc.table_name = $1;
|
|
152
|
+
|
|
153
|
+
-- Get indexes
|
|
154
|
+
SELECT i.relname AS index_name, a.attname AS column_name, ix.indisunique AS is_unique
|
|
155
|
+
FROM pg_class t
|
|
156
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
157
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
158
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
159
|
+
WHERE t.relname = $1;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Step 3: Create Schema Differ
|
|
163
|
+
|
|
164
|
+
**File:** `packages/core/src/base/datasources/differ.ts`
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { getTableConfig, PgTable, PgColumn } from 'drizzle-orm/pg-core';
|
|
168
|
+
|
|
169
|
+
/** Types of schema changes */
|
|
170
|
+
export type TSchemaChange =
|
|
171
|
+
| { type: 'CREATE_TABLE'; table: string; sql: string }
|
|
172
|
+
| { type: 'ADD_COLUMN'; table: string; column: string; sql: string }
|
|
173
|
+
| { type: 'DROP_COLUMN'; table: string; column: string; sql: string }
|
|
174
|
+
| { type: 'ALTER_COLUMN_TYPE'; table: string; column: string; sql: string }
|
|
175
|
+
| { type: 'ALTER_COLUMN_NULL'; table: string; column: string; sql: string }
|
|
176
|
+
| { type: 'ADD_CONSTRAINT'; table: string; constraint: string; sql: string }
|
|
177
|
+
| { type: 'DROP_CONSTRAINT'; table: string; constraint: string; sql: string }
|
|
178
|
+
| { type: 'ADD_INDEX'; table: string; index: string; sql: string }
|
|
179
|
+
| { type: 'DROP_INDEX'; table: string; index: string; sql: string };
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Compares model schema with database schema and generates changes
|
|
183
|
+
*/
|
|
184
|
+
export class SchemaDiffer {
|
|
185
|
+
constructor(private introspector: SchemaIntrospector) {}
|
|
186
|
+
|
|
187
|
+
/** Compare a single model with database and return changes */
|
|
188
|
+
async diffTable(schema: PgTable): Promise<TSchemaChange[]>;
|
|
189
|
+
|
|
190
|
+
/** Compare all models with database */
|
|
191
|
+
async diffAll(schemas: PgTable[]): Promise<TSchemaChange[]>;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Diff Logic:**
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
async diffTable(schema: PgTable): Promise<TSchemaChange[]> {
|
|
199
|
+
const config = getTableConfig(schema);
|
|
200
|
+
const tableName = config.name;
|
|
201
|
+
const changes: TSchemaChange[] = [];
|
|
202
|
+
|
|
203
|
+
const tableExists = await this.introspector.tableExists(tableName);
|
|
204
|
+
|
|
205
|
+
if (!tableExists) {
|
|
206
|
+
// Generate CREATE TABLE
|
|
207
|
+
changes.push({
|
|
208
|
+
type: 'CREATE_TABLE',
|
|
209
|
+
table: tableName,
|
|
210
|
+
sql: this.generateCreateTable(config),
|
|
211
|
+
});
|
|
212
|
+
return changes;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Table exists - compare columns
|
|
216
|
+
const dbColumns = await this.introspector.getColumns(tableName);
|
|
217
|
+
const modelColumns = new Map(Object.entries(config.columns));
|
|
218
|
+
|
|
219
|
+
// Find columns to ADD
|
|
220
|
+
for (const [colName, colDef] of modelColumns) {
|
|
221
|
+
if (!dbColumns.has(colName)) {
|
|
222
|
+
changes.push({
|
|
223
|
+
type: 'ADD_COLUMN',
|
|
224
|
+
table: tableName,
|
|
225
|
+
column: colName,
|
|
226
|
+
sql: `ALTER TABLE "${tableName}" ADD COLUMN ${this.columnToSQL(colName, colDef)}`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Find columns to DROP
|
|
232
|
+
for (const [colName] of dbColumns) {
|
|
233
|
+
if (!modelColumns.has(colName)) {
|
|
234
|
+
changes.push({
|
|
235
|
+
type: 'DROP_COLUMN',
|
|
236
|
+
table: tableName,
|
|
237
|
+
column: colName,
|
|
238
|
+
sql: `ALTER TABLE "${tableName}" DROP COLUMN "${colName}"`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find columns to ALTER
|
|
244
|
+
for (const [colName, colDef] of modelColumns) {
|
|
245
|
+
const dbCol = dbColumns.get(colName);
|
|
246
|
+
if (dbCol) {
|
|
247
|
+
changes.push(...this.diffColumn(tableName, colName, colDef, dbCol));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Diff constraints and indexes...
|
|
252
|
+
return changes;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Step 4: Create Type Mapper
|
|
257
|
+
|
|
258
|
+
**File:** `packages/core/src/base/datasources/type-mapper.ts`
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { PgColumn } from 'drizzle-orm/pg-core';
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Maps between Drizzle column types and PostgreSQL types
|
|
265
|
+
*/
|
|
266
|
+
export class TypeMapper {
|
|
267
|
+
/** Convert Drizzle column to PostgreSQL type string */
|
|
268
|
+
static drizzleToPostgres(column: PgColumn): string {
|
|
269
|
+
const type = column.dataType;
|
|
270
|
+
|
|
271
|
+
switch (type) {
|
|
272
|
+
case 'string': return column.length ? `varchar(${column.length})` : 'text';
|
|
273
|
+
case 'number': return 'integer';
|
|
274
|
+
case 'bigint': return 'bigint';
|
|
275
|
+
case 'boolean': return 'boolean';
|
|
276
|
+
case 'date': return 'timestamp';
|
|
277
|
+
case 'json': return 'jsonb';
|
|
278
|
+
case 'uuid': return 'uuid';
|
|
279
|
+
case 'custom': return column.sqlName; // For custom types
|
|
280
|
+
default: return 'text';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Check if two types are compatible (for ALTER TYPE) */
|
|
285
|
+
static isCompatible(pgType: string, drizzleType: string): boolean;
|
|
286
|
+
|
|
287
|
+
/** Generate USING clause for type conversion if needed */
|
|
288
|
+
static getTypeConversion(from: string, to: string): string | null;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Step 5: Create Schema Migrator
|
|
293
|
+
|
|
294
|
+
**File:** `packages/core/src/base/datasources/migrator.ts`
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { MetadataRegistry } from '@/helpers/inversion';
|
|
298
|
+
import { SchemaIntrospector } from './introspector';
|
|
299
|
+
import { SchemaDiffer } from './differ';
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* LoopBack 4-style schema migrator
|
|
303
|
+
* Syncs model definitions to database without external CLI tools
|
|
304
|
+
*/
|
|
305
|
+
export class SchemaMigrator {
|
|
306
|
+
private introspector: SchemaIntrospector;
|
|
307
|
+
private differ: SchemaDiffer;
|
|
308
|
+
|
|
309
|
+
constructor(private db: NodePgDatabase) {
|
|
310
|
+
this.introspector = new SchemaIntrospector(db);
|
|
311
|
+
this.differ = new SchemaDiffer(this.introspector);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Auto-update schema - safe, incremental changes only
|
|
316
|
+
* Like LoopBack 4's datasource.autoupdate()
|
|
317
|
+
*/
|
|
318
|
+
async autoupdate(opts?: IAutoupdateOptions): Promise<IMigrationResult> {
|
|
319
|
+
const schemas = this.getSchemas(opts?.models);
|
|
320
|
+
const changes = await this.differ.diffAll(schemas);
|
|
321
|
+
|
|
322
|
+
if (opts?.dryRun) {
|
|
323
|
+
return this.buildResult(changes, { executed: false });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return this.executeChanges(changes, opts);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Auto-migrate schema - destructive, drops and recreates
|
|
331
|
+
* Like LoopBack 4's datasource.automigrate()
|
|
332
|
+
*/
|
|
333
|
+
async automigrate(opts?: IAutomigrateOptions): Promise<IMigrationResult> {
|
|
334
|
+
if (!opts?.force) {
|
|
335
|
+
throw getError({
|
|
336
|
+
message: '[automigrate] Destructive operation requires force: true',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const schemas = this.getSchemas(opts?.models);
|
|
341
|
+
|
|
342
|
+
// Drop tables in reverse order (for FK constraints)
|
|
343
|
+
for (const schema of [...schemas].reverse()) {
|
|
344
|
+
const tableName = getTableConfig(schema).name;
|
|
345
|
+
await this.db.execute(sql.raw(`DROP TABLE IF EXISTS "${tableName}" CASCADE`));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Create all tables fresh
|
|
349
|
+
const changes = schemas.map(schema => ({
|
|
350
|
+
type: 'CREATE_TABLE' as const,
|
|
351
|
+
table: getTableConfig(schema).name,
|
|
352
|
+
sql: this.generateCreateTable(schema),
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
return this.executeChanges(changes, opts);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Get schemas from registry or specific models */
|
|
359
|
+
private getSchemas(models?: Array<typeof BaseEntity>): PgTable[] {
|
|
360
|
+
if (models) {
|
|
361
|
+
return models.map(m => m.schema);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const registry = MetadataRegistry.getInstance();
|
|
365
|
+
const allModels = registry.getAllModels();
|
|
366
|
+
return [...allModels.values()].map(entry => entry.schema);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Execute schema changes */
|
|
370
|
+
private async executeChanges(
|
|
371
|
+
changes: TSchemaChange[],
|
|
372
|
+
opts?: { verbose?: boolean }
|
|
373
|
+
): Promise<IMigrationResult>;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Step 6: Integrate with DataSource
|
|
378
|
+
|
|
379
|
+
**File:** `packages/core/src/base/datasources/base.ts`
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
export abstract class BaseDataSource<...> {
|
|
383
|
+
private _migrator?: SchemaMigrator;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get schema migrator instance
|
|
387
|
+
* Like LoopBack 4's datasource property
|
|
388
|
+
*/
|
|
389
|
+
getMigrator(): SchemaMigrator {
|
|
390
|
+
if (!this._migrator) {
|
|
391
|
+
this._migrator = new SchemaMigrator(this.connector);
|
|
392
|
+
}
|
|
393
|
+
return this._migrator;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Convenience method: auto-update schema
|
|
398
|
+
*/
|
|
399
|
+
async autoupdate(opts?: IAutoupdateOptions): Promise<IMigrationResult> {
|
|
400
|
+
return this.getMigrator().autoupdate(opts);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Convenience method: auto-migrate schema (destructive)
|
|
405
|
+
*/
|
|
406
|
+
async automigrate(opts?: IAutomigrateOptions): Promise<IMigrationResult> {
|
|
407
|
+
return this.getMigrator().automigrate(opts);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Files to Create
|
|
415
|
+
|
|
416
|
+
| File | Purpose |
|
|
417
|
+
|------|---------|
|
|
418
|
+
| `packages/core/src/base/datasources/migrator.ts` | Main SchemaMigrator class |
|
|
419
|
+
| `packages/core/src/base/datasources/introspector.ts` | Database schema introspection |
|
|
420
|
+
| `packages/core/src/base/datasources/differ.ts` | Schema comparison logic |
|
|
421
|
+
| `packages/core/src/base/datasources/type-mapper.ts` | Drizzle ↔ PostgreSQL type mapping |
|
|
422
|
+
|
|
423
|
+
## Files to Modify
|
|
424
|
+
|
|
425
|
+
| File | Changes |
|
|
426
|
+
|------|---------|
|
|
427
|
+
| `packages/core/src/base/datasources/types.ts` | Add migration types and interfaces |
|
|
428
|
+
| `packages/core/src/base/datasources/base.ts` | Add `getMigrator()`, `autoupdate()`, `automigrate()` |
|
|
429
|
+
| `packages/core/src/base/datasources/index.ts` | Export new classes |
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Drizzle to PostgreSQL Type Mapping
|
|
434
|
+
|
|
435
|
+
| Drizzle | PostgreSQL | Notes |
|
|
436
|
+
|---------|------------|-------|
|
|
437
|
+
| `text()` | `text` | Variable-length string |
|
|
438
|
+
| `varchar({ length })` | `varchar(n)` | Fixed max length |
|
|
439
|
+
| `integer()` | `integer` | 32-bit signed |
|
|
440
|
+
| `bigint()` | `bigint` | 64-bit signed |
|
|
441
|
+
| `serial()` | `serial` | Auto-increment |
|
|
442
|
+
| `boolean()` | `boolean` | true/false |
|
|
443
|
+
| `timestamp()` | `timestamp` | Date and time |
|
|
444
|
+
| `date()` | `date` | Date only |
|
|
445
|
+
| `json()` | `json` | JSON storage |
|
|
446
|
+
| `jsonb()` | `jsonb` | Binary JSON (indexed) |
|
|
447
|
+
| `uuid()` | `uuid` | UUID type |
|
|
448
|
+
| `numeric({ precision, scale })` | `numeric(p,s)` | Exact decimal |
|
|
449
|
+
| `real()` | `real` | 32-bit float |
|
|
450
|
+
| `doublePrecision()` | `double precision` | 64-bit float |
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Change Detection Matrix
|
|
455
|
+
|
|
456
|
+
| Change Type | Detection Method | SQL Generated |
|
|
457
|
+
|-------------|------------------|---------------|
|
|
458
|
+
| New table | Table not in DB | `CREATE TABLE` |
|
|
459
|
+
| New column | Column not in DB | `ALTER TABLE ADD COLUMN` |
|
|
460
|
+
| Removed column | Column not in model | `ALTER TABLE DROP COLUMN` |
|
|
461
|
+
| Type change | Compare data_type | `ALTER TABLE ALTER COLUMN TYPE` |
|
|
462
|
+
| Nullability | Compare is_nullable | `SET NOT NULL` / `DROP NOT NULL` |
|
|
463
|
+
| Default change | Compare column_default | `SET DEFAULT` / `DROP DEFAULT` |
|
|
464
|
+
| New FK | Constraint not in DB | `ADD CONSTRAINT ... FOREIGN KEY` |
|
|
465
|
+
| Removed FK | Constraint not in model | `DROP CONSTRAINT` |
|
|
466
|
+
| New index | Index not in DB | `CREATE INDEX` |
|
|
467
|
+
| Removed index | Index not in model | `DROP INDEX` |
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Usage Examples
|
|
472
|
+
|
|
473
|
+
### Basic Auto-Update (Recommended)
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// Boot application
|
|
477
|
+
const app = new MyApplication();
|
|
478
|
+
await app.boot();
|
|
479
|
+
|
|
480
|
+
// Auto-update all models
|
|
481
|
+
const dataSource = app.getSync<PostgresDataSource>('datasources.PostgresDataSource');
|
|
482
|
+
const result = await dataSource.autoupdate();
|
|
483
|
+
|
|
484
|
+
console.log('Created tables:', result.created);
|
|
485
|
+
console.log('Altered tables:', result.altered);
|
|
486
|
+
console.log('SQL executed:', result.statements);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Dry Run (Preview Changes)
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const result = await dataSource.autoupdate({ dryRun: true });
|
|
493
|
+
|
|
494
|
+
console.log('Would execute:');
|
|
495
|
+
for (const sql of result.statements) {
|
|
496
|
+
console.log(sql);
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Migrate Specific Models
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { User, Role } from './models';
|
|
504
|
+
|
|
505
|
+
await dataSource.autoupdate({
|
|
506
|
+
models: [User, Role],
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Fresh Database (Development Only)
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
if (process.env.NODE_ENV === 'development') {
|
|
514
|
+
await dataSource.automigrate({ force: true });
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Safety Considerations
|
|
521
|
+
|
|
522
|
+
### autoupdate() Guarantees
|
|
523
|
+
|
|
524
|
+
| Guarantee | Description |
|
|
525
|
+
|-----------|-------------|
|
|
526
|
+
| No table drops | Never drops tables, even if removed from models |
|
|
527
|
+
| No data loss | Column drops are explicit (column must be removed from model) |
|
|
528
|
+
| Transactional | Each ALTER runs in its own transaction |
|
|
529
|
+
| Idempotent | Safe to run multiple times |
|
|
530
|
+
|
|
531
|
+
### Dangerous Operations (Require Confirmation)
|
|
532
|
+
|
|
533
|
+
| Operation | Risk | Mitigation |
|
|
534
|
+
|-----------|------|------------|
|
|
535
|
+
| `DROP COLUMN` | Data loss | Logged with warning |
|
|
536
|
+
| `ALTER TYPE` | May fail if incompatible | Checks compatibility first |
|
|
537
|
+
| `automigrate()` | Drops all tables | Requires `force: true` |
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Future Enhancements
|
|
542
|
+
|
|
543
|
+
1. **Migration History Table** - Track applied migrations in `_ignis_migrations`
|
|
544
|
+
2. **Rollback Support** - Generate reverse migrations
|
|
545
|
+
3. **Data Migrations** - LoopBack 4-style numbered data seed files
|
|
546
|
+
4. **Multi-Schema Support** - Support for PostgreSQL schemas beyond `public`
|
|
547
|
+
5. **MySQL/SQLite Support** - Extend beyond PostgreSQL
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Comparison with Alternatives
|
|
552
|
+
|
|
553
|
+
| Feature | Ignis Migrator | Drizzle Kit | LoopBack 4 |
|
|
554
|
+
|---------|----------------|-------------|------------|
|
|
555
|
+
| CLI required | No | Yes | No |
|
|
556
|
+
| Auto-discovery | Yes (from @model) | No (manual schema file) | Yes |
|
|
557
|
+
| Incremental updates | Yes | Yes | Yes |
|
|
558
|
+
| Dry run | Yes | Yes | No |
|
|
559
|
+
| Type safety | Full | Full | Partial |
|
|
560
|
+
| Random file names | No | Yes | No |
|
|
561
|
+
| Bidirectional | Planned | No | No |
|