@rangka/core 0.1.1 → 0.1.3

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 (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -25
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,133 +0,0 @@
1
- import type { Kysely } from 'kysely';
2
- import { sql } from 'kysely';
3
- import type { SchemaRegistry } from '../schema/registry.js';
4
- import type { DdlOperation } from './types.js';
5
- import { SchemaToDesired } from './desired-state.js';
6
- import { DiffEngine } from './diff-engine.js';
7
- import { introspect } from './introspect.js';
8
-
9
- export interface AutoSyncOptions {
10
- allowDestructive?: boolean;
11
- }
12
-
13
- export interface AutoSyncResult {
14
- applied: DdlOperation[];
15
- warnings: DdlOperation[];
16
- }
17
-
18
- /**
19
- * Compares the desired schema (from the registry) against the actual database state
20
- * and applies any DDL operations needed to bring the database in sync.
21
- */
22
- export async function autoSync(
23
- registry: SchemaRegistry,
24
- db: Kysely<unknown>,
25
- options: AutoSyncOptions = {},
26
- ): Promise<AutoSyncResult> {
27
- const desired = new SchemaToDesired().convert(registry);
28
- const actual = await introspect(db);
29
- const operations = new DiffEngine().diff(desired, actual);
30
-
31
- if (operations.length === 0) {
32
- console.log(`[rangka:sync] Schema is up to date`);
33
- return { applied: [], warnings: [] };
34
- }
35
-
36
- const applied: DdlOperation[] = [];
37
- const warnings: DdlOperation[] = [];
38
-
39
- for (const op of operations) {
40
- if (op.destructive) {
41
- await handleDestructiveOp(op, options, applied, warnings, db);
42
- } else {
43
- await applyOperation(op, db);
44
- applied.push(op);
45
- }
46
- }
47
-
48
- if (applied.length > 0) {
49
- console.log(`[rangka:sync] Applied ${applied.length} operation(s):`);
50
- for (const op of applied) {
51
- console.log(` ${formatOperation(op)}`);
52
- }
53
- }
54
-
55
- if (warnings.length > 0) {
56
- console.warn(`[rangka:sync] ${warnings.length} skipped (destructive):`);
57
- for (const op of warnings) {
58
- console.warn(` ${formatOperation(op)}`);
59
- }
60
- }
61
-
62
- return { applied, warnings };
63
- }
64
-
65
- /** Apply a non-destructive DDL operation. */
66
- async function applyOperation(op: DdlOperation, db: Kysely<unknown>): Promise<void> {
67
- await sql.raw(op.sql).execute(db);
68
- }
69
-
70
- /**
71
- * Handle a destructive operation: apply it if allowed, otherwise log a warning.
72
- */
73
- async function handleDestructiveOp(
74
- op: DdlOperation,
75
- options: AutoSyncOptions,
76
- applied: DdlOperation[],
77
- warnings: DdlOperation[],
78
- db: Kysely<unknown>,
79
- ): Promise<void> {
80
- if (options.allowDestructive) {
81
- const ddlSql = generateDestructiveSql(op);
82
- await sql.raw(ddlSql).execute(db);
83
- applied.push(op);
84
- } else {
85
- warnings.push(op);
86
- }
87
- }
88
-
89
- function formatOperation(op: DdlOperation): string {
90
- switch (op.type) {
91
- case 'CREATE_TABLE':
92
- return `+ table ${op.table}`;
93
- case 'ADD_COLUMN':
94
- return `+ column ${op.table}.${extractColumnFromSql(op.sql)}`;
95
- case 'ALTER_COLUMN_TYPE':
96
- return `~ column type ${op.table}.${extractColumnFromSql(op.sql)}`;
97
- case 'CREATE_INDEX':
98
- return `+ index on ${op.table}`;
99
- case 'ADD_FOREIGN_KEY':
100
- return `+ foreign key on ${op.table}`;
101
- case 'ADD_CHECK_CONSTRAINT':
102
- return `+ check constraint on ${op.table}`;
103
- case 'DROP_TABLE':
104
- return `- table ${op.table} (orphaned)`;
105
- case 'DROP_COLUMN':
106
- return `- column ${op.detail ?? op.table} (orphaned)`;
107
- default:
108
- return `${op.type} on ${op.table}`;
109
- }
110
- }
111
-
112
- function extractColumnFromSql(s: string): string {
113
- const match = s.match(/COLUMN "([^"]+)"/i) ?? s.match(/COLUMN\s+(\S+)/i);
114
- return match ? match[1] : 'unknown';
115
- }
116
-
117
- /** Generate the raw SQL for a destructive operation (DROP COLUMN / DROP TABLE). */
118
- function generateDestructiveSql(op: DdlOperation): string {
119
- switch (op.type) {
120
- case 'DROP_COLUMN': {
121
- const match = op.detail?.match(/Orphaned column: (.+)\.(.+)/);
122
- if (match) return `ALTER TABLE "${match[1]}" DROP COLUMN "${match[2]}"`;
123
- return op.sql;
124
- }
125
- case 'DROP_TABLE': {
126
- const match = op.detail?.match(/Orphaned table: (.+)/);
127
- if (match) return `DROP TABLE "${match[1]}"`;
128
- return op.sql;
129
- }
130
- default:
131
- return op.sql;
132
- }
133
- }
package/src/db/client.ts DELETED
@@ -1,147 +0,0 @@
1
- import { Kysely, PostgresDialect } from 'kysely';
2
- import pg from 'pg';
3
- import type { SchemaRegistry } from '../schema/registry.js';
4
- import { modelToTableName } from './field-mapper.js';
5
-
6
- export interface DatabaseClientConfig {
7
- host: string;
8
- port?: number;
9
- database: string;
10
- user: string;
11
- password: string;
12
- pool?: {
13
- min?: number;
14
- max?: number;
15
- };
16
- }
17
-
18
- /**
19
- * Thin wrapper around Kysely that resolves qualified model names
20
- * (e.g. "sales.Invoice") to their underlying table names automatically.
21
- */
22
- export class DatabaseClient {
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- private readonly db: Kysely<any>;
25
- private readonly pool: pg.Pool;
26
- private readonly qualifiedNameToTable: Map<string, string>;
27
-
28
- constructor(config: DatabaseClientConfig, registry?: SchemaRegistry) {
29
- this.pool = this.createPool(config);
30
- this.db = new Kysely({ dialect: new PostgresDialect({ pool: this.pool }) });
31
- this.qualifiedNameToTable = this.buildTableNameMap(registry);
32
- }
33
-
34
- /**
35
- * Verify the database connection is reachable.
36
- * Throws with actionable diagnostics if it cannot connect.
37
- */
38
- async verifyConnection(): Promise<void> {
39
- let client: pg.PoolClient | undefined;
40
- try {
41
- client = await this.pool.connect();
42
- await client.query('SELECT 1');
43
- } catch (err: unknown) {
44
- const pgErr = err as { code?: string; message?: string };
45
- const detail = formatConnectionError(pgErr, this.pool);
46
- throw new Error(`Database connection failed: ${detail}`, { cause: err });
47
- } finally {
48
- client?.release();
49
- }
50
- }
51
-
52
- // --- Query builders (resolve model name to table name automatically) ---
53
-
54
- selectFrom(model: string) {
55
- return this.db.selectFrom(this.resolveTable(model));
56
- }
57
-
58
- insertInto(model: string) {
59
- return this.db.insertInto(this.resolveTable(model));
60
- }
61
-
62
- updateTable(model: string) {
63
- return this.db.updateTable(this.resolveTable(model));
64
- }
65
-
66
- deleteFrom(model: string) {
67
- return this.db.deleteFrom(this.resolveTable(model));
68
- }
69
-
70
- /** Execute a callback within a database transaction. */
71
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
- async transaction<T>(callback: (trx: Kysely<any>) => Promise<T>): Promise<T> {
73
- return this.db.transaction().execute(callback);
74
- }
75
-
76
- /** Direct access to the underlying Kysely instance. */
77
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
- get kysely(): Kysely<any> {
79
- return this.db;
80
- }
81
-
82
- /** Close the connection pool and release resources. */
83
- async destroy(): Promise<void> {
84
- await this.db.destroy();
85
- }
86
-
87
- // --- Private helpers ---
88
-
89
- /** Maps a qualified model name to its table name, falling back to the raw input. */
90
- private resolveTable(nameOrQualified: string): string {
91
- return this.qualifiedNameToTable.get(nameOrQualified) ?? nameOrQualified;
92
- }
93
-
94
- /** Create the underlying pg connection pool. */
95
- private createPool(config: DatabaseClientConfig): pg.Pool {
96
- return new pg.Pool({
97
- host: config.host,
98
- port: config.port ?? 5432,
99
- database: config.database,
100
- user: config.user,
101
- password: config.password,
102
- min: config.pool?.min ?? 2,
103
- max: config.pool?.max ?? 10,
104
- });
105
- }
106
-
107
- /** Pre-compute the mapping from qualified model names to SQL table names. */
108
- private buildTableNameMap(registry?: SchemaRegistry): Map<string, string> {
109
- const map = new Map<string, string>();
110
- if (registry) {
111
- for (const model of registry.getAllModels()) {
112
- map.set(model.qualifiedName, modelToTableName(model.qualifiedName));
113
- }
114
- }
115
- return map;
116
- }
117
- }
118
-
119
- function formatConnectionError(err: { code?: string; message?: string }, pool: pg.Pool): string {
120
- const opts = (pool as unknown as { options: pg.PoolConfig }).options;
121
- const host = opts.host ?? 'localhost';
122
- const port = opts.port ?? 5432;
123
- const database = opts.database ?? '(unknown)';
124
-
125
- switch (err.code) {
126
- case 'ECONNREFUSED':
127
- return (
128
- `Could not connect to PostgreSQL at ${host}:${port}. ` +
129
- `Is the server running and accepting connections?`
130
- );
131
- case 'ENOTFOUND':
132
- return `Host "${host}" not found. Check the database host configuration.`;
133
- case 'ETIMEDOUT':
134
- return (
135
- `Connection to ${host}:${port} timed out. ` +
136
- `Check network connectivity and firewall rules.`
137
- );
138
- case '3D000':
139
- return `Database "${database}" does not exist on ${host}:${port}.`;
140
- case '28P01':
141
- return `Authentication failed for user "${opts.user ?? '(unknown)'}". Check credentials.`;
142
- case '28000':
143
- return `Authorization failed for user "${opts.user ?? '(unknown)'}". Check pg_hba.conf or user permissions.`;
144
- default:
145
- return err.message ?? 'Unknown error';
146
- }
147
- }
@@ -1,98 +0,0 @@
1
- import type { SchemaRegistry } from '../schema/registry.js';
2
- import type { ResolvedModel } from '../schema/types.js';
3
- import type { DesiredState, TableDefinition, IndexDefinition } from './types.js';
4
- import { mapFieldsToColumns, modelToTableName } from './field-mapper.js';
5
- import { getJobTables } from '../jobs/tables.js';
6
-
7
- /**
8
- * Converts the in-memory schema registry into the desired database state
9
- * (list of table definitions) used by the diff engine for migrations.
10
- */
11
- export class SchemaToDesired {
12
- convert(registry: SchemaRegistry): DesiredState {
13
- const tables: TableDefinition[] = [];
14
- const models = registry.getAllModels();
15
- let hasSequenceField = false;
16
-
17
- for (const model of models) {
18
- const { primaryTable, joinTables } = this.modelToTables(model);
19
- tables.push(primaryTable);
20
- tables.push(...joinTables);
21
-
22
- if (model.fields.some((f) => f.config.type === 'sequence')) {
23
- hasSequenceField = true;
24
- }
25
- }
26
-
27
- // Add the naming_sequence helper table if any model uses sequence fields
28
- if (hasSequenceField) {
29
- tables.push(this.buildNamingSequenceTable());
30
- }
31
-
32
- // Always include the internal job queue tables
33
- tables.push(...getJobTables());
34
-
35
- return { tables };
36
- }
37
-
38
- /**
39
- * Converts a single model into its primary table definition plus any
40
- * extra join/pivot tables required by its fields.
41
- */
42
- private modelToTables(model: ResolvedModel): {
43
- primaryTable: TableDefinition;
44
- joinTables: TableDefinition[];
45
- } {
46
- const tableName = modelToTableName(model.qualifiedName);
47
- const mapping = mapFieldsToColumns(model);
48
- const indexes = this.buildIndexes(model, tableName);
49
-
50
- const primaryTable: TableDefinition = {
51
- name: tableName,
52
- columns: mapping.columns,
53
- foreignKeys: mapping.foreignKeys,
54
- checkConstraints: mapping.checkConstraints,
55
- indexes,
56
- };
57
-
58
- return { primaryTable, joinTables: mapping.extraTables };
59
- }
60
-
61
- /** Generates index definitions from a model's declared indexes. */
62
- private buildIndexes(model: ResolvedModel, tableName: string): IndexDefinition[] {
63
- if (!model.indexes || model.indexes.length === 0) return [];
64
-
65
- return model.indexes.map((idx) => {
66
- const prefix = idx.unique ? 'uidx' : 'idx';
67
- const indexName = `${prefix}_${tableName}_${idx.fields.join('_')}`;
68
- return {
69
- name: indexName,
70
- table: tableName,
71
- columns: idx.fields,
72
- unique: idx.unique ?? false,
73
- };
74
- });
75
- }
76
-
77
- /** Table definition for the naming_sequence counter table. */
78
- private buildNamingSequenceTable(): TableDefinition {
79
- return {
80
- name: 'naming_sequence',
81
- columns: [
82
- { name: 'model', type: 'VARCHAR(255)', nullable: false },
83
- { name: 'field', type: 'VARCHAR(255)', nullable: false },
84
- { name: 'next_val', type: 'BIGINT', nullable: false, defaultValue: '1' },
85
- ],
86
- foreignKeys: [],
87
- checkConstraints: [],
88
- indexes: [
89
- {
90
- name: 'uidx_naming_sequence_key',
91
- table: 'naming_sequence',
92
- columns: ['model', 'field'],
93
- unique: true,
94
- },
95
- ],
96
- };
97
- }
98
- }
@@ -1,305 +0,0 @@
1
- import type {
2
- DesiredState,
3
- ActualState,
4
- DdlOperation,
5
- TableDefinition,
6
- ActualTable,
7
- ColumnDefinition,
8
- ActualColumn,
9
- IndexDefinition,
10
- ActualIndex,
11
- ForeignKeyDefinition,
12
- ActualForeignKey,
13
- CheckConstraintDefinition,
14
- ActualCheckConstraint,
15
- } from './types.js';
16
-
17
- /**
18
- * Compares a desired schema state against the actual database state and
19
- * produces a sorted list of DDL operations needed to reconcile the two.
20
- */
21
- export class DiffEngine {
22
- // --- Public API ---
23
-
24
- /** Compute the full set of DDL operations to bring `actual` in line with `desired`. */
25
- diff(desired: DesiredState, actual: ActualState): DdlOperation[] {
26
- const operations: DdlOperation[] = [];
27
- const actualTablesByName = new Map(actual.tables.map((t) => [t.name, t]));
28
-
29
- // Generate operations for each desired table (create or update)
30
- for (const desiredTable of desired.tables) {
31
- const existingTable = actualTablesByName.get(desiredTable.name);
32
- if (!existingTable) {
33
- operations.push(...this.buildNewTableOperations(desiredTable));
34
- } else {
35
- operations.push(...this.diffTable(desiredTable, existingTable));
36
- }
37
- }
38
-
39
- // Flag tables that exist in the database but not in the schema
40
- for (const existingTable of actual.tables) {
41
- const stillDesired = desired.tables.find((t) => t.name === existingTable.name);
42
- if (!stillDesired) {
43
- operations.push(this.warnOrphanedTable(existingTable.name));
44
- }
45
- }
46
-
47
- return this.sortByDependencyOrder(operations);
48
- }
49
-
50
- // --- Table creation ---
51
-
52
- /** Build all operations needed to create a brand new table (table + indexes + foreign keys + check constraints). */
53
- private buildNewTableOperations(table: TableDefinition): DdlOperation[] {
54
- return [
55
- this.buildCreateTableOp(table),
56
- ...table.indexes.map((idx) => this.buildCreateIndexOp(idx)),
57
- ...table.foreignKeys.map((fk) => this.buildAddForeignKeyOp(table.name, fk)),
58
- ];
59
- }
60
-
61
- /** Generate the CREATE TABLE DDL for a table definition. */
62
- private buildCreateTableOp(table: TableDefinition): DdlOperation {
63
- const columnClauses = table.columns.map((col) => this.columnToSql(col));
64
-
65
- const primaryKeyColumn = table.columns.find((c) => c.primaryKey);
66
- if (primaryKeyColumn) {
67
- columnClauses.push(`PRIMARY KEY (${primaryKeyColumn.name})`);
68
- }
69
-
70
- for (const chk of table.checkConstraints) {
71
- columnClauses.push(`CONSTRAINT "${chk.name}" CHECK (${chk.expression})`);
72
- }
73
-
74
- const sql = `CREATE TABLE "${table.name}" (\n ${columnClauses.join(',\n ')}\n)`;
75
- return {
76
- type: 'CREATE_TABLE',
77
- table: table.name,
78
- sql,
79
- destructive: false,
80
- };
81
- }
82
-
83
- // --- Table diffing (existing table vs desired) ---
84
-
85
- /** Compare an existing table against its desired definition and produce update operations. */
86
- private diffTable(desired: TableDefinition, actual: ActualTable): DdlOperation[] {
87
- const operations: DdlOperation[] = [];
88
-
89
- operations.push(...this.diffColumns(desired.name, desired.columns, actual.columns));
90
- operations.push(...this.diffIndexes(desired.name, desired.indexes, actual.indexes));
91
- operations.push(...this.diffForeignKeys(desired.name, desired.foreignKeys, actual.foreignKeys));
92
- operations.push(
93
- ...this.diffCheckConstraints(desired.name, desired.checkConstraints, actual.checkConstraints),
94
- );
95
-
96
- return operations;
97
- }
98
-
99
- /** Detect added columns, type changes, and orphaned columns. */
100
- private diffColumns(
101
- tableName: string,
102
- desiredColumns: ColumnDefinition[],
103
- actualColumns: ActualColumn[],
104
- ): DdlOperation[] {
105
- const operations: DdlOperation[] = [];
106
- const actualColumnsByName = new Map(actualColumns.map((c) => [c.name, c]));
107
- const desiredColumnNames = new Set(desiredColumns.map((c) => c.name));
108
-
109
- for (const desiredCol of desiredColumns) {
110
- const existingCol = actualColumnsByName.get(desiredCol.name);
111
- if (!existingCol) {
112
- operations.push(this.buildAddColumnOp(tableName, desiredCol));
113
- } else if (this.hasColumnTypeChanged(desiredCol, existingCol)) {
114
- operations.push(this.buildAlterColumnTypeOp(tableName, desiredCol));
115
- }
116
- }
117
-
118
- // Flag columns that exist in the DB but are no longer in the schema
119
- for (const existingCol of actualColumns) {
120
- if (!desiredColumnNames.has(existingCol.name)) {
121
- operations.push(this.warnOrphanedColumn(tableName, existingCol.name));
122
- }
123
- }
124
-
125
- return operations;
126
- }
127
-
128
- /** Detect indexes that need to be created. */
129
- private diffIndexes(
130
- tableName: string,
131
- desiredIndexes: IndexDefinition[],
132
- actualIndexes: ActualIndex[],
133
- ): DdlOperation[] {
134
- const existingIndexNames = new Set(actualIndexes.map((i) => i.name));
135
-
136
- return desiredIndexes
137
- .filter((idx) => !existingIndexNames.has(idx.name))
138
- .map((idx) => this.buildCreateIndexOp(idx));
139
- }
140
-
141
- /** Detect foreign keys that need to be added. */
142
- private diffForeignKeys(
143
- tableName: string,
144
- desiredForeignKeys: ForeignKeyDefinition[],
145
- actualForeignKeys: ActualForeignKey[],
146
- ): DdlOperation[] {
147
- const existingFkNames = new Set(actualForeignKeys.map((fk) => fk.name));
148
-
149
- return desiredForeignKeys
150
- .filter((fk) => !existingFkNames.has(fk.name))
151
- .map((fk) => this.buildAddForeignKeyOp(tableName, fk));
152
- }
153
-
154
- /** Detect CHECK constraints that need to be added. */
155
- private diffCheckConstraints(
156
- tableName: string,
157
- desiredConstraints: CheckConstraintDefinition[],
158
- actualConstraints: ActualCheckConstraint[],
159
- ): DdlOperation[] {
160
- const existingNames = new Set(actualConstraints.map((c) => c.name));
161
-
162
- return desiredConstraints
163
- .filter((chk) => !existingNames.has(chk.name))
164
- .map((chk) => this.buildAddCheckConstraintOp(tableName, chk));
165
- }
166
-
167
- // --- DDL operation builders ---
168
-
169
- /** Generate an ADD COLUMN operation. */
170
- private buildAddColumnOp(tableName: string, col: ColumnDefinition): DdlOperation {
171
- const colSql = this.columnToSql(col);
172
- const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${colSql}`;
173
- return { type: 'ADD_COLUMN', table: tableName, sql, destructive: false };
174
- }
175
-
176
- /** Generate an ALTER COLUMN TYPE operation. */
177
- private buildAlterColumnTypeOp(tableName: string, col: ColumnDefinition): DdlOperation {
178
- const sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${col.name}" TYPE ${col.type}`;
179
- return { type: 'ALTER_COLUMN_TYPE', table: tableName, sql, destructive: false };
180
- }
181
-
182
- /** Generate a CREATE INDEX operation. */
183
- private buildCreateIndexOp(idx: IndexDefinition): DdlOperation {
184
- const uniquePrefix = idx.unique ? 'UNIQUE ' : '';
185
- const quotedColumns = idx.columns.map((c) => `"${c}"`).join(', ');
186
- const whereClause = idx.where ? ` WHERE ${idx.where}` : '';
187
- const sql = `CREATE ${uniquePrefix}INDEX "${idx.name}" ON "${idx.table}" (${quotedColumns})${whereClause}`;
188
- return { type: 'CREATE_INDEX', table: idx.table, sql, destructive: false };
189
- }
190
-
191
- /** Generate an ADD FOREIGN KEY constraint operation. */
192
- private buildAddForeignKeyOp(tableName: string, fk: ForeignKeyDefinition): DdlOperation {
193
- const sql = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${fk.name}" FOREIGN KEY ("${fk.column}") REFERENCES "${fk.referencedTable}" ("${fk.referencedColumn}")`;
194
- return { type: 'ADD_FOREIGN_KEY', table: tableName, sql, destructive: false };
195
- }
196
-
197
- /** Generate an ADD CHECK CONSTRAINT operation. */
198
- private buildAddCheckConstraintOp(
199
- tableName: string,
200
- chk: CheckConstraintDefinition,
201
- ): DdlOperation {
202
- const sql = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${chk.name}" CHECK (${chk.expression})`;
203
- return { type: 'ADD_CHECK_CONSTRAINT', table: tableName, sql, destructive: false };
204
- }
205
-
206
- // --- Orphan warnings (destructive markers) ---
207
-
208
- /** Produce a warning operation for a table that exists in the DB but not in the schema. */
209
- private warnOrphanedTable(tableName: string): DdlOperation {
210
- return {
211
- type: 'DROP_TABLE',
212
- table: tableName,
213
- sql: `-- WARNING: Table "${tableName}" exists in database but not in schema (not dropped without --allow-destructive)`,
214
- destructive: true,
215
- detail: `Orphaned table: ${tableName}`,
216
- };
217
- }
218
-
219
- /** Produce a warning operation for a column that exists in the DB but not in the schema. */
220
- private warnOrphanedColumn(tableName: string, columnName: string): DdlOperation {
221
- return {
222
- type: 'DROP_COLUMN',
223
- table: tableName,
224
- sql: `-- WARNING: Column "${tableName}"."${columnName}" exists in database but not in schema (not dropped without --allow-destructive)`,
225
- destructive: true,
226
- detail: `Orphaned column: ${tableName}.${columnName}`,
227
- };
228
- }
229
-
230
- // --- SQL helpers ---
231
-
232
- /** Convert a column definition to its SQL fragment (e.g. `"name" TEXT NOT NULL DEFAULT 'x'`). */
233
- private columnToSql(col: ColumnDefinition): string {
234
- let sql = `"${col.name}" ${col.type}`;
235
- if (!col.nullable && !col.primaryKey) sql += ' NOT NULL';
236
- if (col.defaultValue !== undefined) sql += ` DEFAULT ${col.defaultValue}`;
237
- return sql;
238
- }
239
-
240
- /** Check whether the column type has changed (case-insensitive comparison). */
241
- private hasColumnTypeChanged(desired: ColumnDefinition, actual: ActualColumn): boolean {
242
- return desired.type.toUpperCase() !== actual.type.toUpperCase();
243
- }
244
-
245
- // --- Operation ordering ---
246
-
247
- /**
248
- * Sort operations so they execute in a safe dependency order:
249
- * 1. CREATE TABLE (tables must exist before columns/indexes reference them)
250
- * 2. ADD COLUMN
251
- * 3. ALTER COLUMN TYPE
252
- * 4. CREATE INDEX (columns must exist first)
253
- * 5. ADD FOREIGN KEY (referenced tables must exist first)
254
- * 6. ADD CHECK CONSTRAINT (columns must exist first)
255
- * 7. Destructive operations (always last, require explicit opt-in)
256
- */
257
- private sortByDependencyOrder(operations: DdlOperation[]): DdlOperation[] {
258
- const createTables: DdlOperation[] = [];
259
- const addColumns: DdlOperation[] = [];
260
- const alterColumns: DdlOperation[] = [];
261
- const createIndexes: DdlOperation[] = [];
262
- const addForeignKeys: DdlOperation[] = [];
263
- const addCheckConstraints: DdlOperation[] = [];
264
- const destructive: DdlOperation[] = [];
265
-
266
- for (const op of operations) {
267
- if (op.destructive) {
268
- destructive.push(op);
269
- } else {
270
- switch (op.type) {
271
- case 'CREATE_TABLE':
272
- createTables.push(op);
273
- break;
274
- case 'ADD_COLUMN':
275
- addColumns.push(op);
276
- break;
277
- case 'ALTER_COLUMN_TYPE':
278
- alterColumns.push(op);
279
- break;
280
- case 'CREATE_INDEX':
281
- createIndexes.push(op);
282
- break;
283
- case 'ADD_FOREIGN_KEY':
284
- addForeignKeys.push(op);
285
- break;
286
- case 'ADD_CHECK_CONSTRAINT':
287
- addCheckConstraints.push(op);
288
- break;
289
- default:
290
- addColumns.push(op);
291
- }
292
- }
293
- }
294
-
295
- return [
296
- ...createTables,
297
- ...addColumns,
298
- ...alterColumns,
299
- ...createIndexes,
300
- ...addForeignKeys,
301
- ...addCheckConstraints,
302
- ...destructive,
303
- ];
304
- }
305
- }