@famgia/omnify-atlas 0.0.1
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/index.cjs +1389 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +585 -0
- package/dist/index.d.ts +585 -0
- package/dist/index.js +1333 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
// src/lock/lock-file.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { readFile, writeFile, stat } from "fs/promises";
|
|
4
|
+
var LOCK_FILE_NAME = ".omnify.lock";
|
|
5
|
+
var LOCK_FILE_VERSION = 1;
|
|
6
|
+
function computeHash(content) {
|
|
7
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
8
|
+
}
|
|
9
|
+
function computeSchemaHash(schema) {
|
|
10
|
+
const content = JSON.stringify({
|
|
11
|
+
name: schema.name,
|
|
12
|
+
kind: schema.kind ?? "object",
|
|
13
|
+
properties: schema.properties ?? {},
|
|
14
|
+
options: schema.options ?? {},
|
|
15
|
+
values: schema.values ?? []
|
|
16
|
+
});
|
|
17
|
+
return computeHash(content);
|
|
18
|
+
}
|
|
19
|
+
function createEmptyLockFile(driver) {
|
|
20
|
+
return {
|
|
21
|
+
version: LOCK_FILE_VERSION,
|
|
22
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23
|
+
driver,
|
|
24
|
+
schemas: {},
|
|
25
|
+
migrations: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function readLockFile(lockFilePath) {
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(lockFilePath, "utf8");
|
|
31
|
+
const parsed = JSON.parse(content);
|
|
32
|
+
if (parsed.version !== LOCK_FILE_VERSION) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Lock file version mismatch: expected ${LOCK_FILE_VERSION}, got ${parsed.version}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === "ENOENT") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function writeLockFile(lockFilePath, lockFile) {
|
|
46
|
+
const content = JSON.stringify(lockFile, null, 2) + "\n";
|
|
47
|
+
await writeFile(lockFilePath, content, "utf8");
|
|
48
|
+
}
|
|
49
|
+
async function buildSchemaHashes(schemas) {
|
|
50
|
+
const hashes = {};
|
|
51
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
52
|
+
const hash = computeSchemaHash(schema);
|
|
53
|
+
let modifiedAt;
|
|
54
|
+
try {
|
|
55
|
+
const stats = await stat(schema.filePath);
|
|
56
|
+
modifiedAt = stats.mtime.toISOString();
|
|
57
|
+
} catch {
|
|
58
|
+
modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
59
|
+
}
|
|
60
|
+
hashes[name] = {
|
|
61
|
+
name,
|
|
62
|
+
hash,
|
|
63
|
+
relativePath: schema.relativePath,
|
|
64
|
+
modifiedAt
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return hashes;
|
|
68
|
+
}
|
|
69
|
+
function compareSchemas(currentHashes, lockFile) {
|
|
70
|
+
const changes = [];
|
|
71
|
+
const unchanged = [];
|
|
72
|
+
const previousHashes = lockFile?.schemas ?? {};
|
|
73
|
+
const previousNames = new Set(Object.keys(previousHashes));
|
|
74
|
+
const currentNames = new Set(Object.keys(currentHashes));
|
|
75
|
+
for (const name of currentNames) {
|
|
76
|
+
if (!previousNames.has(name)) {
|
|
77
|
+
const current = currentHashes[name];
|
|
78
|
+
if (current) {
|
|
79
|
+
changes.push({
|
|
80
|
+
schemaName: name,
|
|
81
|
+
changeType: "added",
|
|
82
|
+
currentHash: current.hash
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const name of previousNames) {
|
|
88
|
+
if (!currentNames.has(name)) {
|
|
89
|
+
const previous = previousHashes[name];
|
|
90
|
+
if (previous) {
|
|
91
|
+
changes.push({
|
|
92
|
+
schemaName: name,
|
|
93
|
+
changeType: "removed",
|
|
94
|
+
previousHash: previous.hash
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const name of currentNames) {
|
|
100
|
+
if (previousNames.has(name)) {
|
|
101
|
+
const current = currentHashes[name];
|
|
102
|
+
const previous = previousHashes[name];
|
|
103
|
+
if (current && previous) {
|
|
104
|
+
if (current.hash !== previous.hash) {
|
|
105
|
+
changes.push({
|
|
106
|
+
schemaName: name,
|
|
107
|
+
changeType: "modified",
|
|
108
|
+
previousHash: previous.hash,
|
|
109
|
+
currentHash: current.hash
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
unchanged.push(name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
hasChanges: changes.length > 0,
|
|
119
|
+
changes,
|
|
120
|
+
unchanged
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function updateLockFile(existingLockFile, currentHashes, driver) {
|
|
124
|
+
return {
|
|
125
|
+
version: LOCK_FILE_VERSION,
|
|
126
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
127
|
+
driver,
|
|
128
|
+
schemas: currentHashes,
|
|
129
|
+
migrations: existingLockFile?.migrations ?? [],
|
|
130
|
+
hclChecksum: existingLockFile?.hclChecksum
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function addMigrationRecord(lockFile, fileName, schemas, migrationContent) {
|
|
134
|
+
const record = {
|
|
135
|
+
fileName,
|
|
136
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
137
|
+
schemas,
|
|
138
|
+
checksum: computeHash(migrationContent)
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
...lockFile,
|
|
142
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
143
|
+
migrations: [...lockFile.migrations, record]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/hcl/type-mapper.ts
|
|
148
|
+
var MYSQL_TYPES = {
|
|
149
|
+
String: (prop) => ({
|
|
150
|
+
type: `varchar(${prop.length ?? 255})`,
|
|
151
|
+
nullable: prop.nullable ?? false,
|
|
152
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
153
|
+
}),
|
|
154
|
+
Int: (prop) => ({
|
|
155
|
+
type: "int",
|
|
156
|
+
nullable: prop.nullable ?? false,
|
|
157
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0,
|
|
158
|
+
unsigned: prop.unsigned ?? false
|
|
159
|
+
}),
|
|
160
|
+
BigInt: (prop) => ({
|
|
161
|
+
type: "bigint",
|
|
162
|
+
nullable: prop.nullable ?? false,
|
|
163
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0,
|
|
164
|
+
unsigned: prop.unsigned ?? false
|
|
165
|
+
}),
|
|
166
|
+
Float: (prop) => ({
|
|
167
|
+
type: "double",
|
|
168
|
+
nullable: prop.nullable ?? false,
|
|
169
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
170
|
+
}),
|
|
171
|
+
Boolean: (prop) => ({
|
|
172
|
+
type: "tinyint(1)",
|
|
173
|
+
nullable: prop.nullable ?? false,
|
|
174
|
+
default: prop.default !== void 0 ? prop.default ? "1" : "0" : void 0
|
|
175
|
+
}),
|
|
176
|
+
Text: (prop) => ({
|
|
177
|
+
type: "text",
|
|
178
|
+
nullable: prop.nullable ?? false
|
|
179
|
+
}),
|
|
180
|
+
LongText: (prop) => ({
|
|
181
|
+
type: "longtext",
|
|
182
|
+
nullable: prop.nullable ?? false
|
|
183
|
+
}),
|
|
184
|
+
Date: (prop) => ({
|
|
185
|
+
type: "date",
|
|
186
|
+
nullable: prop.nullable ?? false
|
|
187
|
+
}),
|
|
188
|
+
Time: (prop) => ({
|
|
189
|
+
type: "time",
|
|
190
|
+
nullable: prop.nullable ?? false
|
|
191
|
+
}),
|
|
192
|
+
Timestamp: (prop) => ({
|
|
193
|
+
type: "timestamp",
|
|
194
|
+
nullable: prop.nullable ?? false
|
|
195
|
+
}),
|
|
196
|
+
Json: (prop) => ({
|
|
197
|
+
type: "json",
|
|
198
|
+
nullable: prop.nullable ?? false
|
|
199
|
+
}),
|
|
200
|
+
Email: (prop) => ({
|
|
201
|
+
type: "varchar(255)",
|
|
202
|
+
nullable: prop.nullable ?? false
|
|
203
|
+
}),
|
|
204
|
+
Password: (prop) => ({
|
|
205
|
+
type: "varchar(255)",
|
|
206
|
+
nullable: prop.nullable ?? false
|
|
207
|
+
}),
|
|
208
|
+
File: (prop) => ({
|
|
209
|
+
type: "varchar(500)",
|
|
210
|
+
nullable: prop.nullable ?? false
|
|
211
|
+
}),
|
|
212
|
+
MultiFile: (prop) => ({
|
|
213
|
+
type: "json",
|
|
214
|
+
nullable: prop.nullable ?? false
|
|
215
|
+
}),
|
|
216
|
+
Enum: (prop) => {
|
|
217
|
+
const enumProp = prop;
|
|
218
|
+
const values = enumProp.enum ?? [];
|
|
219
|
+
const enumDef = values.map((v) => `'${v}'`).join(", ");
|
|
220
|
+
return {
|
|
221
|
+
type: `enum(${enumDef})`,
|
|
222
|
+
nullable: prop.nullable ?? false,
|
|
223
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
Select: (prop) => ({
|
|
227
|
+
type: "varchar(100)",
|
|
228
|
+
nullable: prop.nullable ?? false
|
|
229
|
+
}),
|
|
230
|
+
Lookup: (prop) => ({
|
|
231
|
+
type: "bigint",
|
|
232
|
+
nullable: prop.nullable ?? false,
|
|
233
|
+
unsigned: true
|
|
234
|
+
})
|
|
235
|
+
};
|
|
236
|
+
var POSTGRES_TYPES = {
|
|
237
|
+
String: (prop) => ({
|
|
238
|
+
type: `varchar(${prop.length ?? 255})`,
|
|
239
|
+
nullable: prop.nullable ?? false,
|
|
240
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
241
|
+
}),
|
|
242
|
+
Int: (prop) => ({
|
|
243
|
+
type: "integer",
|
|
244
|
+
nullable: prop.nullable ?? false,
|
|
245
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
246
|
+
}),
|
|
247
|
+
BigInt: (prop) => ({
|
|
248
|
+
type: "bigint",
|
|
249
|
+
nullable: prop.nullable ?? false,
|
|
250
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
251
|
+
}),
|
|
252
|
+
Float: (prop) => ({
|
|
253
|
+
type: "double precision",
|
|
254
|
+
nullable: prop.nullable ?? false,
|
|
255
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
256
|
+
}),
|
|
257
|
+
Boolean: (prop) => ({
|
|
258
|
+
type: "boolean",
|
|
259
|
+
nullable: prop.nullable ?? false,
|
|
260
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
261
|
+
}),
|
|
262
|
+
Text: (prop) => ({
|
|
263
|
+
type: "text",
|
|
264
|
+
nullable: prop.nullable ?? false
|
|
265
|
+
}),
|
|
266
|
+
LongText: (prop) => ({
|
|
267
|
+
type: "text",
|
|
268
|
+
nullable: prop.nullable ?? false
|
|
269
|
+
}),
|
|
270
|
+
Date: (prop) => ({
|
|
271
|
+
type: "date",
|
|
272
|
+
nullable: prop.nullable ?? false
|
|
273
|
+
}),
|
|
274
|
+
Time: (prop) => ({
|
|
275
|
+
type: "time",
|
|
276
|
+
nullable: prop.nullable ?? false
|
|
277
|
+
}),
|
|
278
|
+
Timestamp: (prop) => ({
|
|
279
|
+
type: "timestamp",
|
|
280
|
+
nullable: prop.nullable ?? false
|
|
281
|
+
}),
|
|
282
|
+
Json: (prop) => ({
|
|
283
|
+
type: "jsonb",
|
|
284
|
+
nullable: prop.nullable ?? false
|
|
285
|
+
}),
|
|
286
|
+
Email: (prop) => ({
|
|
287
|
+
type: "varchar(255)",
|
|
288
|
+
nullable: prop.nullable ?? false
|
|
289
|
+
}),
|
|
290
|
+
Password: (prop) => ({
|
|
291
|
+
type: "varchar(255)",
|
|
292
|
+
nullable: prop.nullable ?? false
|
|
293
|
+
}),
|
|
294
|
+
File: (prop) => ({
|
|
295
|
+
type: "varchar(500)",
|
|
296
|
+
nullable: prop.nullable ?? false
|
|
297
|
+
}),
|
|
298
|
+
MultiFile: (prop) => ({
|
|
299
|
+
type: "jsonb",
|
|
300
|
+
nullable: prop.nullable ?? false
|
|
301
|
+
}),
|
|
302
|
+
// For PostgreSQL, enums are separate types
|
|
303
|
+
Enum: (prop) => ({
|
|
304
|
+
type: "varchar(100)",
|
|
305
|
+
nullable: prop.nullable ?? false,
|
|
306
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
307
|
+
}),
|
|
308
|
+
Select: (prop) => ({
|
|
309
|
+
type: "varchar(100)",
|
|
310
|
+
nullable: prop.nullable ?? false
|
|
311
|
+
}),
|
|
312
|
+
Lookup: (prop) => ({
|
|
313
|
+
type: "bigint",
|
|
314
|
+
nullable: prop.nullable ?? false
|
|
315
|
+
})
|
|
316
|
+
};
|
|
317
|
+
var SQLITE_TYPES = {
|
|
318
|
+
String: (prop) => ({
|
|
319
|
+
type: "text",
|
|
320
|
+
nullable: prop.nullable ?? false,
|
|
321
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
322
|
+
}),
|
|
323
|
+
Int: (prop) => ({
|
|
324
|
+
type: "integer",
|
|
325
|
+
nullable: prop.nullable ?? false,
|
|
326
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
327
|
+
}),
|
|
328
|
+
BigInt: (prop) => ({
|
|
329
|
+
type: "integer",
|
|
330
|
+
nullable: prop.nullable ?? false,
|
|
331
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
332
|
+
}),
|
|
333
|
+
Float: (prop) => ({
|
|
334
|
+
type: "real",
|
|
335
|
+
nullable: prop.nullable ?? false,
|
|
336
|
+
default: prop.default !== void 0 ? String(prop.default) : void 0
|
|
337
|
+
}),
|
|
338
|
+
Boolean: (prop) => ({
|
|
339
|
+
type: "integer",
|
|
340
|
+
nullable: prop.nullable ?? false,
|
|
341
|
+
default: prop.default !== void 0 ? prop.default ? "1" : "0" : void 0
|
|
342
|
+
}),
|
|
343
|
+
Text: (prop) => ({
|
|
344
|
+
type: "text",
|
|
345
|
+
nullable: prop.nullable ?? false
|
|
346
|
+
}),
|
|
347
|
+
LongText: (prop) => ({
|
|
348
|
+
type: "text",
|
|
349
|
+
nullable: prop.nullable ?? false
|
|
350
|
+
}),
|
|
351
|
+
Date: (prop) => ({
|
|
352
|
+
type: "text",
|
|
353
|
+
nullable: prop.nullable ?? false
|
|
354
|
+
}),
|
|
355
|
+
Time: (prop) => ({
|
|
356
|
+
type: "text",
|
|
357
|
+
nullable: prop.nullable ?? false
|
|
358
|
+
}),
|
|
359
|
+
Timestamp: (prop) => ({
|
|
360
|
+
type: "text",
|
|
361
|
+
nullable: prop.nullable ?? false
|
|
362
|
+
}),
|
|
363
|
+
Json: (prop) => ({
|
|
364
|
+
type: "text",
|
|
365
|
+
nullable: prop.nullable ?? false
|
|
366
|
+
}),
|
|
367
|
+
Email: (prop) => ({
|
|
368
|
+
type: "text",
|
|
369
|
+
nullable: prop.nullable ?? false
|
|
370
|
+
}),
|
|
371
|
+
Password: (prop) => ({
|
|
372
|
+
type: "text",
|
|
373
|
+
nullable: prop.nullable ?? false
|
|
374
|
+
}),
|
|
375
|
+
File: (prop) => ({
|
|
376
|
+
type: "text",
|
|
377
|
+
nullable: prop.nullable ?? false
|
|
378
|
+
}),
|
|
379
|
+
MultiFile: (prop) => ({
|
|
380
|
+
type: "text",
|
|
381
|
+
nullable: prop.nullable ?? false
|
|
382
|
+
}),
|
|
383
|
+
Enum: (prop) => ({
|
|
384
|
+
type: "text",
|
|
385
|
+
nullable: prop.nullable ?? false,
|
|
386
|
+
default: prop.default !== void 0 ? `'${prop.default}'` : void 0
|
|
387
|
+
}),
|
|
388
|
+
Select: (prop) => ({
|
|
389
|
+
type: "text",
|
|
390
|
+
nullable: prop.nullable ?? false
|
|
391
|
+
}),
|
|
392
|
+
Lookup: (prop) => ({
|
|
393
|
+
type: "integer",
|
|
394
|
+
nullable: prop.nullable ?? false
|
|
395
|
+
})
|
|
396
|
+
};
|
|
397
|
+
var DRIVER_TYPE_MAPS = {
|
|
398
|
+
mysql: MYSQL_TYPES,
|
|
399
|
+
postgres: POSTGRES_TYPES,
|
|
400
|
+
pgsql: POSTGRES_TYPES,
|
|
401
|
+
// Alias for postgres
|
|
402
|
+
sqlite: SQLITE_TYPES,
|
|
403
|
+
mariadb: MYSQL_TYPES,
|
|
404
|
+
// MariaDB uses same types as MySQL
|
|
405
|
+
sqlsrv: MYSQL_TYPES
|
|
406
|
+
// SQL Server uses similar types to MySQL for now
|
|
407
|
+
};
|
|
408
|
+
function mapPropertyToSql(property, driver) {
|
|
409
|
+
const typeMap = DRIVER_TYPE_MAPS[driver];
|
|
410
|
+
const mapper = typeMap[property.type];
|
|
411
|
+
const baseProp = property;
|
|
412
|
+
if (mapper) {
|
|
413
|
+
return mapper(baseProp);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
type: "varchar(255)",
|
|
417
|
+
nullable: baseProp.nullable ?? false
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function getPrimaryKeyType(pkType, driver) {
|
|
421
|
+
switch (pkType) {
|
|
422
|
+
case "Int":
|
|
423
|
+
return {
|
|
424
|
+
type: driver === "postgres" ? "serial" : "int",
|
|
425
|
+
nullable: false,
|
|
426
|
+
autoIncrement: driver !== "postgres",
|
|
427
|
+
unsigned: driver === "mysql" || driver === "mariadb"
|
|
428
|
+
};
|
|
429
|
+
case "BigInt":
|
|
430
|
+
return {
|
|
431
|
+
type: driver === "postgres" ? "bigserial" : "bigint",
|
|
432
|
+
nullable: false,
|
|
433
|
+
autoIncrement: driver !== "postgres",
|
|
434
|
+
unsigned: driver === "mysql" || driver === "mariadb"
|
|
435
|
+
};
|
|
436
|
+
case "Uuid":
|
|
437
|
+
return {
|
|
438
|
+
type: driver === "postgres" ? "uuid" : "char(36)",
|
|
439
|
+
nullable: false
|
|
440
|
+
};
|
|
441
|
+
case "String":
|
|
442
|
+
return {
|
|
443
|
+
type: "varchar(255)",
|
|
444
|
+
nullable: false
|
|
445
|
+
};
|
|
446
|
+
default:
|
|
447
|
+
return {
|
|
448
|
+
type: driver === "postgres" ? "bigserial" : "bigint",
|
|
449
|
+
nullable: false,
|
|
450
|
+
autoIncrement: driver !== "postgres",
|
|
451
|
+
unsigned: driver === "mysql" || driver === "mariadb"
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function getTimestampType(driver) {
|
|
456
|
+
return {
|
|
457
|
+
type: driver === "postgres" ? "timestamp" : "timestamp",
|
|
458
|
+
nullable: true
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function schemaNameToTableName(schemaName) {
|
|
462
|
+
const snakeCase = schemaName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
463
|
+
if (snakeCase.endsWith("y")) {
|
|
464
|
+
return snakeCase.slice(0, -1) + "ies";
|
|
465
|
+
} else if (snakeCase.endsWith("s") || snakeCase.endsWith("x") || snakeCase.endsWith("ch") || snakeCase.endsWith("sh")) {
|
|
466
|
+
return snakeCase + "es";
|
|
467
|
+
} else {
|
|
468
|
+
return snakeCase + "s";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function propertyNameToColumnName(propertyName) {
|
|
472
|
+
return propertyName.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/hcl/generator.ts
|
|
476
|
+
function generateHclTable(schema, allSchemas, driver) {
|
|
477
|
+
const tableName = schemaNameToTableName(schema.name);
|
|
478
|
+
const columns = [];
|
|
479
|
+
const indexes = [];
|
|
480
|
+
const foreignKeys = [];
|
|
481
|
+
const pkType = schema.options?.primaryKeyType ?? "BigInt";
|
|
482
|
+
columns.push({
|
|
483
|
+
name: "id",
|
|
484
|
+
type: getPrimaryKeyType(pkType, driver),
|
|
485
|
+
primaryKey: true
|
|
486
|
+
});
|
|
487
|
+
if (schema.properties) {
|
|
488
|
+
for (const [propName, property] of Object.entries(schema.properties)) {
|
|
489
|
+
if (property.type === "Association") {
|
|
490
|
+
const assocProp = property;
|
|
491
|
+
if (assocProp.relation === "ManyToOne" || assocProp.relation === "OneToOne") {
|
|
492
|
+
const columnName2 = propertyNameToColumnName(propName) + "_id";
|
|
493
|
+
const targetSchema = assocProp.target ? allSchemas[assocProp.target] : void 0;
|
|
494
|
+
const targetTable = assocProp.target ? schemaNameToTableName(assocProp.target) : "unknown";
|
|
495
|
+
const targetPkType = targetSchema?.options?.primaryKeyType ?? "BigInt";
|
|
496
|
+
const fkType = getPrimaryKeyType(
|
|
497
|
+
targetPkType,
|
|
498
|
+
driver
|
|
499
|
+
);
|
|
500
|
+
const isNullable = assocProp.relation === "ManyToOne";
|
|
501
|
+
columns.push({
|
|
502
|
+
name: columnName2,
|
|
503
|
+
type: {
|
|
504
|
+
...fkType,
|
|
505
|
+
nullable: isNullable,
|
|
506
|
+
autoIncrement: false
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
foreignKeys.push({
|
|
510
|
+
name: `fk_${tableName}_${columnName2}`,
|
|
511
|
+
columns: [columnName2],
|
|
512
|
+
refTable: targetTable,
|
|
513
|
+
refColumns: ["id"],
|
|
514
|
+
onDelete: assocProp.onDelete ?? "RESTRICT",
|
|
515
|
+
onUpdate: assocProp.onUpdate ?? "CASCADE"
|
|
516
|
+
});
|
|
517
|
+
indexes.push({
|
|
518
|
+
name: `idx_${tableName}_${columnName2}`,
|
|
519
|
+
columns: [columnName2]
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const baseProp = property;
|
|
525
|
+
const columnName = propertyNameToColumnName(propName);
|
|
526
|
+
const sqlType = mapPropertyToSql(property, driver);
|
|
527
|
+
columns.push({
|
|
528
|
+
name: columnName,
|
|
529
|
+
type: sqlType,
|
|
530
|
+
unique: baseProp.unique ?? false
|
|
531
|
+
});
|
|
532
|
+
if (baseProp.unique) {
|
|
533
|
+
indexes.push({
|
|
534
|
+
name: `idx_${tableName}_${columnName}_unique`,
|
|
535
|
+
columns: [columnName],
|
|
536
|
+
unique: true
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (schema.options?.timestamps !== false) {
|
|
542
|
+
const timestampType = getTimestampType(driver);
|
|
543
|
+
columns.push(
|
|
544
|
+
{ name: "created_at", type: timestampType },
|
|
545
|
+
{ name: "updated_at", type: timestampType }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (schema.options?.softDelete) {
|
|
549
|
+
columns.push({
|
|
550
|
+
name: "deleted_at",
|
|
551
|
+
type: getTimestampType(driver)
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (schema.options?.indexes) {
|
|
555
|
+
for (const index of schema.options.indexes) {
|
|
556
|
+
const indexColumns = index.columns.map(propertyNameToColumnName);
|
|
557
|
+
indexes.push({
|
|
558
|
+
name: index.name ?? `idx_${tableName}_${indexColumns.join("_")}`,
|
|
559
|
+
columns: indexColumns,
|
|
560
|
+
unique: index.unique ?? false
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (schema.options?.unique) {
|
|
565
|
+
const uniqueConstraints = Array.isArray(schema.options.unique[0]) ? schema.options.unique : [schema.options.unique];
|
|
566
|
+
for (const constraint of uniqueConstraints) {
|
|
567
|
+
const constraintColumns = constraint.map(propertyNameToColumnName);
|
|
568
|
+
indexes.push({
|
|
569
|
+
name: `idx_${tableName}_${constraintColumns.join("_")}_unique`,
|
|
570
|
+
columns: constraintColumns,
|
|
571
|
+
unique: true
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
name: tableName,
|
|
577
|
+
columns,
|
|
578
|
+
indexes,
|
|
579
|
+
foreignKeys
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function generateHclSchema(schemas, options) {
|
|
583
|
+
const tables = [];
|
|
584
|
+
for (const schema of Object.values(schemas)) {
|
|
585
|
+
if (schema.kind === "enum") {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const table = generateHclTable(schema, schemas, options.driver);
|
|
589
|
+
tables.push(table);
|
|
590
|
+
}
|
|
591
|
+
const enums = Object.values(schemas).filter((s) => s.kind === "enum").map((s) => ({
|
|
592
|
+
name: s.name.toLowerCase(),
|
|
593
|
+
values: s.values ?? []
|
|
594
|
+
}));
|
|
595
|
+
return {
|
|
596
|
+
driver: options.driver,
|
|
597
|
+
schemaName: options.schemaName,
|
|
598
|
+
tables,
|
|
599
|
+
enums
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function formatHclColumn(column, driver) {
|
|
603
|
+
const parts = [` column "${column.name}" {`];
|
|
604
|
+
parts.push(` type = ${formatSqlType(column.type.type, driver)}`);
|
|
605
|
+
if (column.type.nullable) {
|
|
606
|
+
parts.push(" null = true");
|
|
607
|
+
}
|
|
608
|
+
if (column.type.default !== void 0) {
|
|
609
|
+
parts.push(` default = ${column.type.default}`);
|
|
610
|
+
}
|
|
611
|
+
if (column.type.autoIncrement) {
|
|
612
|
+
parts.push(" auto_increment = true");
|
|
613
|
+
}
|
|
614
|
+
if (column.type.unsigned && (driver === "mysql" || driver === "mariadb")) {
|
|
615
|
+
parts.push(" unsigned = true");
|
|
616
|
+
}
|
|
617
|
+
parts.push(" }");
|
|
618
|
+
return parts.join("\n");
|
|
619
|
+
}
|
|
620
|
+
function formatSqlType(type, driver) {
|
|
621
|
+
if (type.startsWith("enum(")) {
|
|
622
|
+
if (driver === "mysql" || driver === "mariadb") {
|
|
623
|
+
return type;
|
|
624
|
+
}
|
|
625
|
+
return "varchar(100)";
|
|
626
|
+
}
|
|
627
|
+
return type;
|
|
628
|
+
}
|
|
629
|
+
function formatHclIndex(index) {
|
|
630
|
+
const columns = index.columns.map((c) => `"${c}"`).join(", ");
|
|
631
|
+
const unique = index.unique ? "unique = true\n " : "";
|
|
632
|
+
return ` index "${index.name}" {
|
|
633
|
+
columns = [${columns}]
|
|
634
|
+
${unique}}`;
|
|
635
|
+
}
|
|
636
|
+
function formatHclForeignKey(fk) {
|
|
637
|
+
const columns = fk.columns.map((c) => `"${c}"`).join(", ");
|
|
638
|
+
const refColumns = fk.refColumns.map((c) => `"${c}"`).join(", ");
|
|
639
|
+
return ` foreign_key "${fk.name}" {
|
|
640
|
+
columns = [${columns}]
|
|
641
|
+
ref_columns = [${refColumns}]
|
|
642
|
+
on_update = ${fk.onUpdate ?? "CASCADE"}
|
|
643
|
+
on_delete = ${fk.onDelete ?? "RESTRICT"}
|
|
644
|
+
}`;
|
|
645
|
+
}
|
|
646
|
+
function renderHcl(schema) {
|
|
647
|
+
const lines = [];
|
|
648
|
+
const schemaPrefix = schema.schemaName ? `schema "${schema.schemaName}" {
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
` : "";
|
|
652
|
+
lines.push(schemaPrefix);
|
|
653
|
+
for (const table of schema.tables) {
|
|
654
|
+
lines.push(`table "${table.name}" {`);
|
|
655
|
+
if (schema.schemaName) {
|
|
656
|
+
lines.push(` schema = schema.${schema.schemaName}`);
|
|
657
|
+
}
|
|
658
|
+
for (const column of table.columns) {
|
|
659
|
+
lines.push(formatHclColumn(column, schema.driver));
|
|
660
|
+
}
|
|
661
|
+
const pkColumn = table.columns.find((c) => c.primaryKey);
|
|
662
|
+
if (pkColumn) {
|
|
663
|
+
lines.push(` primary_key {
|
|
664
|
+
columns = ["${pkColumn.name}"]
|
|
665
|
+
}`);
|
|
666
|
+
}
|
|
667
|
+
for (const index of table.indexes) {
|
|
668
|
+
lines.push(formatHclIndex(index));
|
|
669
|
+
}
|
|
670
|
+
for (const fk of table.foreignKeys) {
|
|
671
|
+
lines.push(formatHclForeignKey(fk));
|
|
672
|
+
}
|
|
673
|
+
lines.push("}\n");
|
|
674
|
+
}
|
|
675
|
+
return lines.join("\n");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/atlas/runner.ts
|
|
679
|
+
import { execa } from "execa";
|
|
680
|
+
import { mkdir, writeFile as writeFile2, rm } from "fs/promises";
|
|
681
|
+
import { join } from "path";
|
|
682
|
+
import { tmpdir } from "os";
|
|
683
|
+
import { randomUUID } from "crypto";
|
|
684
|
+
import { atlasError, atlasNotFoundError } from "@famgia/omnify-core";
|
|
685
|
+
var DEFAULT_CONFIG = {
|
|
686
|
+
binaryPath: "atlas",
|
|
687
|
+
timeout: 6e4
|
|
688
|
+
// 60 seconds
|
|
689
|
+
};
|
|
690
|
+
function getSchemaUrl(_driver, path) {
|
|
691
|
+
return `file://${path}`;
|
|
692
|
+
}
|
|
693
|
+
function normalizeDevUrl(devUrl, driver) {
|
|
694
|
+
if (devUrl === "docker") {
|
|
695
|
+
switch (driver) {
|
|
696
|
+
case "mysql":
|
|
697
|
+
return "docker://mysql/8/dev";
|
|
698
|
+
case "mariadb":
|
|
699
|
+
return "docker://mariadb/latest/dev";
|
|
700
|
+
case "postgres":
|
|
701
|
+
return "docker://postgres/15/dev";
|
|
702
|
+
default:
|
|
703
|
+
return devUrl;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return devUrl;
|
|
707
|
+
}
|
|
708
|
+
async function createTempDir() {
|
|
709
|
+
const tempPath = join(tmpdir(), `omnify-atlas-${randomUUID()}`);
|
|
710
|
+
await mkdir(tempPath, { recursive: true });
|
|
711
|
+
return tempPath;
|
|
712
|
+
}
|
|
713
|
+
async function executeAtlas(config, args) {
|
|
714
|
+
const binaryPath = config.binaryPath ?? DEFAULT_CONFIG.binaryPath;
|
|
715
|
+
const timeout = config.timeout ?? DEFAULT_CONFIG.timeout;
|
|
716
|
+
const startTime = Date.now();
|
|
717
|
+
try {
|
|
718
|
+
const result = config.workDir ? await execa(binaryPath, args, {
|
|
719
|
+
timeout,
|
|
720
|
+
reject: false,
|
|
721
|
+
cwd: config.workDir
|
|
722
|
+
}) : await execa(binaryPath, args, {
|
|
723
|
+
timeout,
|
|
724
|
+
reject: false
|
|
725
|
+
});
|
|
726
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
727
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
728
|
+
return {
|
|
729
|
+
success: result.exitCode === 0,
|
|
730
|
+
stdout,
|
|
731
|
+
stderr,
|
|
732
|
+
exitCode: result.exitCode ?? 0,
|
|
733
|
+
duration: Date.now() - startTime
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const err = error;
|
|
737
|
+
if (err.code === "ENOENT") {
|
|
738
|
+
throw atlasNotFoundError();
|
|
739
|
+
}
|
|
740
|
+
throw atlasError(`Failed to execute Atlas: ${err.message}`, err);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function checkAtlasVersion(config = {}) {
|
|
744
|
+
const fullConfig = {
|
|
745
|
+
...DEFAULT_CONFIG,
|
|
746
|
+
...config,
|
|
747
|
+
driver: config.driver ?? "mysql",
|
|
748
|
+
devUrl: config.devUrl ?? ""
|
|
749
|
+
};
|
|
750
|
+
try {
|
|
751
|
+
const result = await executeAtlas(fullConfig, ["version"]);
|
|
752
|
+
if (result.success) {
|
|
753
|
+
const match = result.stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
754
|
+
return {
|
|
755
|
+
version: match?.[1] ?? result.stdout.trim(),
|
|
756
|
+
available: true
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
version: "",
|
|
761
|
+
available: false
|
|
762
|
+
};
|
|
763
|
+
} catch {
|
|
764
|
+
return {
|
|
765
|
+
version: "",
|
|
766
|
+
available: false
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function runAtlasDiff(config, options) {
|
|
771
|
+
const devUrl = normalizeDevUrl(config.devUrl, config.driver);
|
|
772
|
+
const toUrl = getSchemaUrl(config.driver, options.toPath);
|
|
773
|
+
const args = [
|
|
774
|
+
"schema",
|
|
775
|
+
"diff",
|
|
776
|
+
"--dev-url",
|
|
777
|
+
devUrl,
|
|
778
|
+
"--to",
|
|
779
|
+
toUrl,
|
|
780
|
+
"--format",
|
|
781
|
+
'{{ sql . " " }}'
|
|
782
|
+
];
|
|
783
|
+
if (options.fromPath) {
|
|
784
|
+
const fromUrl = getSchemaUrl(config.driver, options.fromPath);
|
|
785
|
+
args.push("--from", fromUrl);
|
|
786
|
+
}
|
|
787
|
+
const result = await executeAtlas(config, args);
|
|
788
|
+
const sql = result.stdout.trim();
|
|
789
|
+
const hasChanges = sql.length > 0 && !sql.includes("-- No changes");
|
|
790
|
+
return {
|
|
791
|
+
...result,
|
|
792
|
+
hasChanges,
|
|
793
|
+
sql: hasChanges ? sql : ""
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function diffHclSchemas(config, fromHcl, toHcl) {
|
|
797
|
+
const tempDir = await createTempDir();
|
|
798
|
+
try {
|
|
799
|
+
const toPath = join(tempDir, "to.hcl");
|
|
800
|
+
await writeFile2(toPath, toHcl, "utf8");
|
|
801
|
+
let fromPath;
|
|
802
|
+
if (fromHcl) {
|
|
803
|
+
fromPath = join(tempDir, "from.hcl");
|
|
804
|
+
await writeFile2(fromPath, fromHcl, "utf8");
|
|
805
|
+
}
|
|
806
|
+
return await runAtlasDiff(config, {
|
|
807
|
+
fromPath,
|
|
808
|
+
toPath
|
|
809
|
+
});
|
|
810
|
+
} finally {
|
|
811
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function validateHcl(config, hclPath) {
|
|
815
|
+
const devUrl = normalizeDevUrl(config.devUrl, config.driver);
|
|
816
|
+
return executeAtlas(config, [
|
|
817
|
+
"schema",
|
|
818
|
+
"inspect",
|
|
819
|
+
"--dev-url",
|
|
820
|
+
devUrl,
|
|
821
|
+
"--url",
|
|
822
|
+
getSchemaUrl(config.driver, hclPath),
|
|
823
|
+
"--format",
|
|
824
|
+
"{{ sql . }}"
|
|
825
|
+
]);
|
|
826
|
+
}
|
|
827
|
+
async function applySchema(config, hclPath) {
|
|
828
|
+
const devUrl = normalizeDevUrl(config.devUrl, config.driver);
|
|
829
|
+
return executeAtlas(config, [
|
|
830
|
+
"schema",
|
|
831
|
+
"apply",
|
|
832
|
+
"--dev-url",
|
|
833
|
+
devUrl,
|
|
834
|
+
"--to",
|
|
835
|
+
getSchemaUrl(config.driver, hclPath),
|
|
836
|
+
"--auto-approve"
|
|
837
|
+
]);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/diff/parser.ts
|
|
841
|
+
var PATTERNS = {
|
|
842
|
+
createTable: /^CREATE TABLE\s+[`"]?(\w+)[`"]?/i,
|
|
843
|
+
dropTable: /^DROP TABLE\s+(?:IF EXISTS\s+)?[`"]?(\w+)[`"]?/i,
|
|
844
|
+
alterTable: /^ALTER TABLE\s+[`"]?(\w+)[`"]?/i,
|
|
845
|
+
addColumn: /ADD\s+(?:COLUMN\s+)?[`"]?(\w+)[`"]?/i,
|
|
846
|
+
dropColumn: /DROP\s+(?:COLUMN\s+)?[`"]?(\w+)[`"]?/i,
|
|
847
|
+
modifyColumn: /MODIFY\s+(?:COLUMN\s+)?[`"]?(\w+)[`"]?/i,
|
|
848
|
+
changeColumn: /CHANGE\s+(?:COLUMN\s+)?[`"]?(\w+)[`"]?/i,
|
|
849
|
+
alterColumn: /ALTER\s+(?:COLUMN\s+)?[`"]?(\w+)[`"]?/i,
|
|
850
|
+
createIndex: /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+[`"]?(\w+)[`"]?\s+ON\s+[`"]?(\w+)[`"]?/i,
|
|
851
|
+
dropIndex: /^DROP\s+INDEX\s+[`"]?(\w+)[`"]?(?:\s+ON\s+[`"]?(\w+)[`"]?)?/i,
|
|
852
|
+
addConstraint: /ADD\s+CONSTRAINT\s+[`"]?(\w+)[`"]?/i,
|
|
853
|
+
dropConstraint: /DROP\s+CONSTRAINT\s+[`"]?(\w+)[`"]?/i,
|
|
854
|
+
addForeignKey: /ADD\s+(?:CONSTRAINT\s+[`"]?\w+[`"]?\s+)?FOREIGN KEY/i,
|
|
855
|
+
dropForeignKey: /DROP\s+FOREIGN KEY\s+[`"]?(\w+)[`"]?/i
|
|
856
|
+
};
|
|
857
|
+
function splitStatements(sql) {
|
|
858
|
+
const statements = [];
|
|
859
|
+
let current = "";
|
|
860
|
+
let inString = false;
|
|
861
|
+
let stringChar = "";
|
|
862
|
+
for (let i = 0; i < sql.length; i++) {
|
|
863
|
+
const char = sql[i];
|
|
864
|
+
if ((char === "'" || char === '"') && sql[i - 1] !== "\\") {
|
|
865
|
+
if (!inString) {
|
|
866
|
+
inString = true;
|
|
867
|
+
stringChar = char;
|
|
868
|
+
} else if (char === stringChar) {
|
|
869
|
+
inString = false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (char === ";" && !inString) {
|
|
873
|
+
const stmt = current.trim();
|
|
874
|
+
if (stmt) {
|
|
875
|
+
statements.push(stmt);
|
|
876
|
+
}
|
|
877
|
+
current = "";
|
|
878
|
+
} else {
|
|
879
|
+
current += char;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const final = current.trim();
|
|
883
|
+
if (final) {
|
|
884
|
+
statements.push(final);
|
|
885
|
+
}
|
|
886
|
+
return statements;
|
|
887
|
+
}
|
|
888
|
+
function parseStatement(sql) {
|
|
889
|
+
const trimmedSql = sql.trim();
|
|
890
|
+
let match = trimmedSql.match(PATTERNS.createTable);
|
|
891
|
+
if (match && match[1]) {
|
|
892
|
+
return {
|
|
893
|
+
sql: trimmedSql,
|
|
894
|
+
type: "CREATE_TABLE",
|
|
895
|
+
tableName: match[1],
|
|
896
|
+
severity: "safe"
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
match = trimmedSql.match(PATTERNS.dropTable);
|
|
900
|
+
if (match && match[1]) {
|
|
901
|
+
return {
|
|
902
|
+
sql: trimmedSql,
|
|
903
|
+
type: "DROP_TABLE",
|
|
904
|
+
tableName: match[1],
|
|
905
|
+
severity: "destructive"
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
match = trimmedSql.match(PATTERNS.createIndex);
|
|
909
|
+
if (match && match[1] && match[2]) {
|
|
910
|
+
return {
|
|
911
|
+
sql: trimmedSql,
|
|
912
|
+
type: "CREATE_INDEX",
|
|
913
|
+
tableName: match[2],
|
|
914
|
+
indexName: match[1],
|
|
915
|
+
severity: "safe"
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
match = trimmedSql.match(PATTERNS.dropIndex);
|
|
919
|
+
if (match && match[1]) {
|
|
920
|
+
return {
|
|
921
|
+
sql: trimmedSql,
|
|
922
|
+
type: "DROP_INDEX",
|
|
923
|
+
tableName: match[2] ?? "",
|
|
924
|
+
indexName: match[1],
|
|
925
|
+
severity: "warning"
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
match = trimmedSql.match(PATTERNS.alterTable);
|
|
929
|
+
if (match && match[1]) {
|
|
930
|
+
const tableName = match[1];
|
|
931
|
+
const alterPart = trimmedSql.slice(match[0].length);
|
|
932
|
+
const addColMatch = alterPart.match(PATTERNS.addColumn);
|
|
933
|
+
if (addColMatch && addColMatch[1]) {
|
|
934
|
+
return {
|
|
935
|
+
sql: trimmedSql,
|
|
936
|
+
type: "ADD_COLUMN",
|
|
937
|
+
tableName,
|
|
938
|
+
columnName: addColMatch[1],
|
|
939
|
+
severity: "safe"
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const dropColMatch = alterPart.match(PATTERNS.dropColumn);
|
|
943
|
+
if (dropColMatch && dropColMatch[1]) {
|
|
944
|
+
return {
|
|
945
|
+
sql: trimmedSql,
|
|
946
|
+
type: "DROP_COLUMN",
|
|
947
|
+
tableName,
|
|
948
|
+
columnName: dropColMatch[1],
|
|
949
|
+
severity: "destructive"
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const modifyColMatch = alterPart.match(PATTERNS.modifyColumn) || alterPart.match(PATTERNS.changeColumn) || alterPart.match(PATTERNS.alterColumn);
|
|
953
|
+
if (modifyColMatch && modifyColMatch[1]) {
|
|
954
|
+
return {
|
|
955
|
+
sql: trimmedSql,
|
|
956
|
+
type: "MODIFY_COLUMN",
|
|
957
|
+
tableName,
|
|
958
|
+
columnName: modifyColMatch[1],
|
|
959
|
+
severity: "warning"
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
if (PATTERNS.addForeignKey.test(alterPart)) {
|
|
963
|
+
const constraintMatch = alterPart.match(PATTERNS.addConstraint);
|
|
964
|
+
const fkConstraintName = constraintMatch?.[1];
|
|
965
|
+
if (fkConstraintName) {
|
|
966
|
+
return {
|
|
967
|
+
sql: trimmedSql,
|
|
968
|
+
type: "ADD_FOREIGN_KEY",
|
|
969
|
+
tableName,
|
|
970
|
+
constraintName: fkConstraintName,
|
|
971
|
+
severity: "safe"
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
sql: trimmedSql,
|
|
976
|
+
type: "ADD_FOREIGN_KEY",
|
|
977
|
+
tableName,
|
|
978
|
+
severity: "safe"
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const dropFkMatch = alterPart.match(PATTERNS.dropForeignKey);
|
|
982
|
+
if (dropFkMatch && dropFkMatch[1]) {
|
|
983
|
+
return {
|
|
984
|
+
sql: trimmedSql,
|
|
985
|
+
type: "DROP_FOREIGN_KEY",
|
|
986
|
+
tableName,
|
|
987
|
+
constraintName: dropFkMatch[1],
|
|
988
|
+
severity: "warning"
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const addConstraintMatch = alterPart.match(PATTERNS.addConstraint);
|
|
992
|
+
if (addConstraintMatch && addConstraintMatch[1]) {
|
|
993
|
+
return {
|
|
994
|
+
sql: trimmedSql,
|
|
995
|
+
type: "ADD_CONSTRAINT",
|
|
996
|
+
tableName,
|
|
997
|
+
constraintName: addConstraintMatch[1],
|
|
998
|
+
severity: "safe"
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const dropConstraintMatch = alterPart.match(PATTERNS.dropConstraint);
|
|
1002
|
+
if (dropConstraintMatch && dropConstraintMatch[1]) {
|
|
1003
|
+
return {
|
|
1004
|
+
sql: trimmedSql,
|
|
1005
|
+
type: "DROP_CONSTRAINT",
|
|
1006
|
+
tableName,
|
|
1007
|
+
constraintName: dropConstraintMatch[1],
|
|
1008
|
+
severity: "warning"
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
sql: trimmedSql,
|
|
1013
|
+
type: "ALTER_TABLE",
|
|
1014
|
+
tableName,
|
|
1015
|
+
severity: "warning"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
sql: trimmedSql,
|
|
1020
|
+
type: "UNKNOWN",
|
|
1021
|
+
tableName: "",
|
|
1022
|
+
severity: "warning"
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function createEmptyTableChange(tableName) {
|
|
1026
|
+
return {
|
|
1027
|
+
tableName,
|
|
1028
|
+
isNew: false,
|
|
1029
|
+
isDropped: false,
|
|
1030
|
+
addedColumns: [],
|
|
1031
|
+
droppedColumns: [],
|
|
1032
|
+
modifiedColumns: [],
|
|
1033
|
+
addedIndexes: [],
|
|
1034
|
+
droppedIndexes: [],
|
|
1035
|
+
addedForeignKeys: [],
|
|
1036
|
+
droppedForeignKeys: []
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function getOrCreateTable(tables, tableName) {
|
|
1040
|
+
const existing = tables[tableName];
|
|
1041
|
+
if (existing) {
|
|
1042
|
+
return existing;
|
|
1043
|
+
}
|
|
1044
|
+
const newTable = createEmptyTableChange(tableName);
|
|
1045
|
+
tables[tableName] = newTable;
|
|
1046
|
+
return newTable;
|
|
1047
|
+
}
|
|
1048
|
+
function groupByTable(statements) {
|
|
1049
|
+
const tables = {};
|
|
1050
|
+
for (const stmt of statements) {
|
|
1051
|
+
if (!stmt.tableName) continue;
|
|
1052
|
+
const table = getOrCreateTable(tables, stmt.tableName);
|
|
1053
|
+
switch (stmt.type) {
|
|
1054
|
+
case "CREATE_TABLE":
|
|
1055
|
+
tables[stmt.tableName] = { ...table, isNew: true };
|
|
1056
|
+
break;
|
|
1057
|
+
case "DROP_TABLE":
|
|
1058
|
+
tables[stmt.tableName] = { ...table, isDropped: true };
|
|
1059
|
+
break;
|
|
1060
|
+
case "ADD_COLUMN":
|
|
1061
|
+
if (stmt.columnName) {
|
|
1062
|
+
tables[stmt.tableName] = {
|
|
1063
|
+
...table,
|
|
1064
|
+
addedColumns: [...table.addedColumns, stmt.columnName]
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
1068
|
+
case "DROP_COLUMN":
|
|
1069
|
+
if (stmt.columnName) {
|
|
1070
|
+
tables[stmt.tableName] = {
|
|
1071
|
+
...table,
|
|
1072
|
+
droppedColumns: [...table.droppedColumns, stmt.columnName]
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
case "MODIFY_COLUMN":
|
|
1077
|
+
if (stmt.columnName) {
|
|
1078
|
+
tables[stmt.tableName] = {
|
|
1079
|
+
...table,
|
|
1080
|
+
modifiedColumns: [...table.modifiedColumns, stmt.columnName]
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
break;
|
|
1084
|
+
case "CREATE_INDEX":
|
|
1085
|
+
if (stmt.indexName) {
|
|
1086
|
+
tables[stmt.tableName] = {
|
|
1087
|
+
...table,
|
|
1088
|
+
addedIndexes: [...table.addedIndexes, stmt.indexName]
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
break;
|
|
1092
|
+
case "DROP_INDEX":
|
|
1093
|
+
if (stmt.indexName) {
|
|
1094
|
+
tables[stmt.tableName] = {
|
|
1095
|
+
...table,
|
|
1096
|
+
droppedIndexes: [...table.droppedIndexes, stmt.indexName]
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
break;
|
|
1100
|
+
case "ADD_FOREIGN_KEY":
|
|
1101
|
+
tables[stmt.tableName] = {
|
|
1102
|
+
...table,
|
|
1103
|
+
addedForeignKeys: [
|
|
1104
|
+
...table.addedForeignKeys,
|
|
1105
|
+
stmt.constraintName ?? "unnamed"
|
|
1106
|
+
]
|
|
1107
|
+
};
|
|
1108
|
+
break;
|
|
1109
|
+
case "DROP_FOREIGN_KEY":
|
|
1110
|
+
if (stmt.constraintName) {
|
|
1111
|
+
tables[stmt.tableName] = {
|
|
1112
|
+
...table,
|
|
1113
|
+
droppedForeignKeys: [...table.droppedForeignKeys, stmt.constraintName]
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return tables;
|
|
1120
|
+
}
|
|
1121
|
+
function calculateSummary(statements) {
|
|
1122
|
+
return {
|
|
1123
|
+
totalStatements: statements.length,
|
|
1124
|
+
tablesCreated: statements.filter((s) => s.type === "CREATE_TABLE").length,
|
|
1125
|
+
tablesDropped: statements.filter((s) => s.type === "DROP_TABLE").length,
|
|
1126
|
+
tablesAltered: statements.filter((s) => s.type === "ALTER_TABLE").length,
|
|
1127
|
+
columnsAdded: statements.filter((s) => s.type === "ADD_COLUMN").length,
|
|
1128
|
+
columnsDropped: statements.filter((s) => s.type === "DROP_COLUMN").length,
|
|
1129
|
+
columnsModified: statements.filter((s) => s.type === "MODIFY_COLUMN").length,
|
|
1130
|
+
indexesAdded: statements.filter((s) => s.type === "CREATE_INDEX").length,
|
|
1131
|
+
indexesDropped: statements.filter((s) => s.type === "DROP_INDEX").length,
|
|
1132
|
+
foreignKeysAdded: statements.filter((s) => s.type === "ADD_FOREIGN_KEY").length,
|
|
1133
|
+
foreignKeysDropped: statements.filter((s) => s.type === "DROP_FOREIGN_KEY").length
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function parseDiffOutput(sql) {
|
|
1137
|
+
const trimmedSql = sql.trim();
|
|
1138
|
+
if (!trimmedSql || trimmedSql === "-- No changes") {
|
|
1139
|
+
return {
|
|
1140
|
+
hasChanges: false,
|
|
1141
|
+
hasDestructiveChanges: false,
|
|
1142
|
+
statements: [],
|
|
1143
|
+
tableChanges: {},
|
|
1144
|
+
summary: {
|
|
1145
|
+
totalStatements: 0,
|
|
1146
|
+
tablesCreated: 0,
|
|
1147
|
+
tablesDropped: 0,
|
|
1148
|
+
tablesAltered: 0,
|
|
1149
|
+
columnsAdded: 0,
|
|
1150
|
+
columnsDropped: 0,
|
|
1151
|
+
columnsModified: 0,
|
|
1152
|
+
indexesAdded: 0,
|
|
1153
|
+
indexesDropped: 0,
|
|
1154
|
+
foreignKeysAdded: 0,
|
|
1155
|
+
foreignKeysDropped: 0
|
|
1156
|
+
},
|
|
1157
|
+
rawSql: trimmedSql
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const sqlWithoutComments = trimmedSql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n");
|
|
1161
|
+
const rawStatements = splitStatements(sqlWithoutComments);
|
|
1162
|
+
const statements = rawStatements.map(parseStatement);
|
|
1163
|
+
const hasDestructiveChanges = statements.some(
|
|
1164
|
+
(s) => s.severity === "destructive"
|
|
1165
|
+
);
|
|
1166
|
+
return {
|
|
1167
|
+
hasChanges: statements.length > 0,
|
|
1168
|
+
hasDestructiveChanges,
|
|
1169
|
+
statements,
|
|
1170
|
+
tableChanges: groupByTable(statements),
|
|
1171
|
+
summary: calculateSummary(statements),
|
|
1172
|
+
rawSql: trimmedSql
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function formatDiffSummary(result) {
|
|
1176
|
+
if (!result.hasChanges) {
|
|
1177
|
+
return "No schema changes detected.";
|
|
1178
|
+
}
|
|
1179
|
+
const lines = ["Schema changes detected:"];
|
|
1180
|
+
const { summary } = result;
|
|
1181
|
+
if (summary.tablesCreated > 0) {
|
|
1182
|
+
lines.push(` + ${summary.tablesCreated} table(s) created`);
|
|
1183
|
+
}
|
|
1184
|
+
if (summary.tablesDropped > 0) {
|
|
1185
|
+
lines.push(` - ${summary.tablesDropped} table(s) dropped [DESTRUCTIVE]`);
|
|
1186
|
+
}
|
|
1187
|
+
if (summary.columnsAdded > 0) {
|
|
1188
|
+
lines.push(` + ${summary.columnsAdded} column(s) added`);
|
|
1189
|
+
}
|
|
1190
|
+
if (summary.columnsDropped > 0) {
|
|
1191
|
+
lines.push(` - ${summary.columnsDropped} column(s) dropped [DESTRUCTIVE]`);
|
|
1192
|
+
}
|
|
1193
|
+
if (summary.columnsModified > 0) {
|
|
1194
|
+
lines.push(` ~ ${summary.columnsModified} column(s) modified`);
|
|
1195
|
+
}
|
|
1196
|
+
if (summary.indexesAdded > 0) {
|
|
1197
|
+
lines.push(` + ${summary.indexesAdded} index(es) added`);
|
|
1198
|
+
}
|
|
1199
|
+
if (summary.indexesDropped > 0) {
|
|
1200
|
+
lines.push(` - ${summary.indexesDropped} index(es) dropped`);
|
|
1201
|
+
}
|
|
1202
|
+
if (summary.foreignKeysAdded > 0) {
|
|
1203
|
+
lines.push(` + ${summary.foreignKeysAdded} foreign key(s) added`);
|
|
1204
|
+
}
|
|
1205
|
+
if (summary.foreignKeysDropped > 0) {
|
|
1206
|
+
lines.push(` - ${summary.foreignKeysDropped} foreign key(s) dropped`);
|
|
1207
|
+
}
|
|
1208
|
+
lines.push("");
|
|
1209
|
+
lines.push(`Total: ${summary.totalStatements} statement(s)`);
|
|
1210
|
+
if (result.hasDestructiveChanges) {
|
|
1211
|
+
lines.push("");
|
|
1212
|
+
lines.push("WARNING: This diff contains destructive changes!");
|
|
1213
|
+
}
|
|
1214
|
+
return lines.join("\n");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/preview/preview.ts
|
|
1218
|
+
import { join as join2 } from "path";
|
|
1219
|
+
import { atlasNotFoundError as atlasNotFoundError2 } from "@famgia/omnify-core";
|
|
1220
|
+
async function generatePreview(schemas, atlasConfig, options = {}) {
|
|
1221
|
+
const atlasVersion = await checkAtlasVersion(atlasConfig);
|
|
1222
|
+
if (!atlasVersion.available) {
|
|
1223
|
+
throw atlasNotFoundError2();
|
|
1224
|
+
}
|
|
1225
|
+
const currentHashes = await buildSchemaHashes(schemas);
|
|
1226
|
+
const lockFilePath = join2(atlasConfig.workDir ?? process.cwd(), LOCK_FILE_NAME);
|
|
1227
|
+
const existingLockFile = await readLockFile(lockFilePath);
|
|
1228
|
+
const schemaChanges = compareSchemas(currentHashes, existingLockFile);
|
|
1229
|
+
const currentHcl = renderHcl(
|
|
1230
|
+
generateHclSchema(schemas, {
|
|
1231
|
+
driver: atlasConfig.driver
|
|
1232
|
+
})
|
|
1233
|
+
);
|
|
1234
|
+
let previousHcl = null;
|
|
1235
|
+
if (existingLockFile?.hclChecksum) {
|
|
1236
|
+
previousHcl = null;
|
|
1237
|
+
}
|
|
1238
|
+
const atlasDiff = await diffHclSchemas(atlasConfig, previousHcl, currentHcl);
|
|
1239
|
+
const databaseChanges = parseDiffOutput(atlasDiff.sql);
|
|
1240
|
+
const summary = buildSummary(schemaChanges, databaseChanges, options);
|
|
1241
|
+
return {
|
|
1242
|
+
hasChanges: schemaChanges.hasChanges || databaseChanges.hasChanges,
|
|
1243
|
+
hasDestructiveChanges: databaseChanges.hasDestructiveChanges,
|
|
1244
|
+
schemaChanges,
|
|
1245
|
+
databaseChanges,
|
|
1246
|
+
summary,
|
|
1247
|
+
sql: atlasDiff.sql
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function buildSummary(schemaChanges, databaseChanges, options) {
|
|
1251
|
+
const lines = [];
|
|
1252
|
+
if (!schemaChanges.hasChanges && !databaseChanges.hasChanges) {
|
|
1253
|
+
return "No changes detected. Schema is up to date.";
|
|
1254
|
+
}
|
|
1255
|
+
if (schemaChanges.hasChanges) {
|
|
1256
|
+
lines.push("Schema file changes:");
|
|
1257
|
+
for (const change of schemaChanges.changes) {
|
|
1258
|
+
const icon = change.changeType === "added" ? "+" : change.changeType === "removed" ? "-" : "~";
|
|
1259
|
+
lines.push(` ${icon} ${change.schemaName} (${change.changeType})`);
|
|
1260
|
+
}
|
|
1261
|
+
lines.push("");
|
|
1262
|
+
}
|
|
1263
|
+
if (databaseChanges.hasChanges) {
|
|
1264
|
+
lines.push(formatDiffSummary(databaseChanges));
|
|
1265
|
+
}
|
|
1266
|
+
if (options.warnDestructive && databaseChanges.hasDestructiveChanges) {
|
|
1267
|
+
lines.push("");
|
|
1268
|
+
lines.push("\u26A0\uFE0F WARNING: This preview contains destructive changes!");
|
|
1269
|
+
lines.push(" Review carefully before generating migrations.");
|
|
1270
|
+
}
|
|
1271
|
+
return lines.join("\n");
|
|
1272
|
+
}
|
|
1273
|
+
async function previewSchemaChanges(schemas, lockFilePath) {
|
|
1274
|
+
const currentHashes = await buildSchemaHashes(schemas);
|
|
1275
|
+
const existingLockFile = await readLockFile(lockFilePath);
|
|
1276
|
+
return compareSchemas(currentHashes, existingLockFile);
|
|
1277
|
+
}
|
|
1278
|
+
function formatPreview(preview, format = "text") {
|
|
1279
|
+
switch (format) {
|
|
1280
|
+
case "json":
|
|
1281
|
+
return JSON.stringify(preview, null, 2);
|
|
1282
|
+
case "minimal":
|
|
1283
|
+
if (!preview.hasChanges) {
|
|
1284
|
+
return "No changes";
|
|
1285
|
+
}
|
|
1286
|
+
const parts = [];
|
|
1287
|
+
const { summary } = preview.databaseChanges;
|
|
1288
|
+
if (summary.tablesCreated > 0) parts.push(`+${summary.tablesCreated} tables`);
|
|
1289
|
+
if (summary.tablesDropped > 0) parts.push(`-${summary.tablesDropped} tables`);
|
|
1290
|
+
if (summary.columnsAdded > 0) parts.push(`+${summary.columnsAdded} columns`);
|
|
1291
|
+
if (summary.columnsDropped > 0) parts.push(`-${summary.columnsDropped} columns`);
|
|
1292
|
+
return parts.join(", ") || "Changes detected";
|
|
1293
|
+
case "text":
|
|
1294
|
+
default:
|
|
1295
|
+
return preview.summary;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function hasBlockingIssues(_preview) {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
export {
|
|
1302
|
+
LOCK_FILE_NAME,
|
|
1303
|
+
LOCK_FILE_VERSION,
|
|
1304
|
+
addMigrationRecord,
|
|
1305
|
+
applySchema,
|
|
1306
|
+
buildSchemaHashes,
|
|
1307
|
+
checkAtlasVersion,
|
|
1308
|
+
compareSchemas,
|
|
1309
|
+
computeHash,
|
|
1310
|
+
computeSchemaHash,
|
|
1311
|
+
createEmptyLockFile,
|
|
1312
|
+
diffHclSchemas,
|
|
1313
|
+
formatDiffSummary,
|
|
1314
|
+
formatPreview,
|
|
1315
|
+
generateHclSchema,
|
|
1316
|
+
generateHclTable,
|
|
1317
|
+
generatePreview,
|
|
1318
|
+
getPrimaryKeyType,
|
|
1319
|
+
getTimestampType,
|
|
1320
|
+
hasBlockingIssues,
|
|
1321
|
+
mapPropertyToSql,
|
|
1322
|
+
parseDiffOutput,
|
|
1323
|
+
previewSchemaChanges,
|
|
1324
|
+
propertyNameToColumnName,
|
|
1325
|
+
readLockFile,
|
|
1326
|
+
renderHcl,
|
|
1327
|
+
runAtlasDiff,
|
|
1328
|
+
schemaNameToTableName,
|
|
1329
|
+
updateLockFile,
|
|
1330
|
+
validateHcl,
|
|
1331
|
+
writeLockFile
|
|
1332
|
+
};
|
|
1333
|
+
//# sourceMappingURL=index.js.map
|