@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.
Files changed (34) hide show
  1. package/LICENSE.md +1 -0
  2. package/package.json +2 -2
  3. package/wiki/changelogs/{v0.0.1-7-initial-architecture.md → 2025-12-16-initial-architecture.md} +20 -12
  4. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
  5. package/wiki/changelogs/2025-12-17-refactor.md +80 -12
  6. package/wiki/changelogs/2025-12-18-performance-optimizations.md +28 -90
  7. package/wiki/changelogs/2025-12-18-repository-validation-security.md +101 -297
  8. package/wiki/changelogs/index.md +20 -8
  9. package/wiki/changelogs/planned-schema-migrator.md +561 -0
  10. package/wiki/changelogs/planned-transaction-support.md +216 -0
  11. package/wiki/changelogs/template.md +123 -0
  12. package/wiki/get-started/best-practices/api-usage-examples.md +0 -2
  13. package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
  14. package/wiki/get-started/best-practices/code-style-standards.md +575 -10
  15. package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
  16. package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
  17. package/wiki/get-started/best-practices/data-modeling.md +91 -34
  18. package/wiki/get-started/best-practices/security-guidelines.md +3 -1
  19. package/wiki/get-started/building-a-crud-api.md +3 -3
  20. package/wiki/get-started/core-concepts/application.md +72 -3
  21. package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
  22. package/wiki/get-started/core-concepts/components.md +4 -2
  23. package/wiki/get-started/core-concepts/persistent.md +350 -378
  24. package/wiki/get-started/core-concepts/services.md +21 -27
  25. package/wiki/references/base/bootstrapping.md +789 -0
  26. package/wiki/references/base/components.md +1 -1
  27. package/wiki/references/base/dependency-injection.md +95 -2
  28. package/wiki/references/base/services.md +2 -2
  29. package/wiki/references/components/authentication.md +4 -3
  30. package/wiki/references/components/index.md +1 -1
  31. package/wiki/references/helpers/error.md +2 -2
  32. package/wiki/references/src-details/boot.md +379 -0
  33. package/wiki/references/src-details/core.md +2 -2
  34. 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 |