@uql/core 3.4.2 → 3.4.4
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/CHANGELOG.md +6 -1
- package/README.md +44 -29
- package/dist/browser/uql-browser.min.js +1289 -0
- package/dist/browser/uql-browser.min.js.map +1 -1
- package/package.json +2 -2
|
@@ -1012,3 +1012,1292 @@ const hooks = {
|
|
|
1012
1012
|
|
|
1013
1013
|
export { BaseEntity, _Company as Company, _InventoryAdjustment as InventoryAdjustment, _Item as Item, _ItemAdjustment as ItemAdjustment, _ItemTag as ItemTag, _LedgerAccount as LedgerAccount, _MeasureUnit as MeasureUnit, _MeasureUnitCategory as MeasureUnitCategory, _Profile as Profile, _Storehouse as Storehouse, _Tag as Tag, _Tax as Tax, _TaxCategory as TaxCategory, _User as User, _UserWithNonUpdatableId as UserWithNonUpdatableId, clearTables, createSpec, createTables, dropTables };
|
|
1014
1014
|
//# sourceMappingURL=uql-browser.min.js.map
|
|
1015
|
+
nst querier = await this.getQuerier();
|
|
1016
|
+
try {
|
|
1017
|
+
const sql = `
|
|
1018
|
+
SELECT table_name
|
|
1019
|
+
FROM information_schema.tables
|
|
1020
|
+
WHERE table_schema = 'public'
|
|
1021
|
+
AND table_type = 'BASE TABLE'
|
|
1022
|
+
ORDER BY table_name
|
|
1023
|
+
`;
|
|
1024
|
+
const results = await querier.all(sql);
|
|
1025
|
+
return results.map((r)=>r.table_name);
|
|
1026
|
+
} finally{
|
|
1027
|
+
await querier.release();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async tableExists(tableName) {
|
|
1031
|
+
const querier = await this.getQuerier();
|
|
1032
|
+
try {
|
|
1033
|
+
return this.tableExistsInternal(querier, tableName);
|
|
1034
|
+
} finally{
|
|
1035
|
+
await querier.release();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async tableExistsInternal(querier, tableName) {
|
|
1039
|
+
const sql = `
|
|
1040
|
+
SELECT EXISTS (
|
|
1041
|
+
SELECT FROM information_schema.tables
|
|
1042
|
+
WHERE table_schema = 'public'
|
|
1043
|
+
AND table_name = $1
|
|
1044
|
+
) AS exists
|
|
1045
|
+
`;
|
|
1046
|
+
const results = await querier.all(sql, [
|
|
1047
|
+
tableName
|
|
1048
|
+
]);
|
|
1049
|
+
return results[0]?.exists ?? false;
|
|
1050
|
+
}
|
|
1051
|
+
async getQuerier() {
|
|
1052
|
+
const querier = await this.pool.getQuerier();
|
|
1053
|
+
if (!isSqlQuerier(querier)) {
|
|
1054
|
+
await querier.release();
|
|
1055
|
+
throw new Error('PostgresSchemaIntrospector requires a SQL-based querier');
|
|
1056
|
+
}
|
|
1057
|
+
return querier;
|
|
1058
|
+
}
|
|
1059
|
+
async getColumns(querier, tableName) {
|
|
1060
|
+
const sql = /*sql*/ `
|
|
1061
|
+
SELECT
|
|
1062
|
+
c.column_name,
|
|
1063
|
+
c.data_type,
|
|
1064
|
+
c.udt_name,
|
|
1065
|
+
c.is_nullable,
|
|
1066
|
+
c.column_default,
|
|
1067
|
+
c.character_maximum_length,
|
|
1068
|
+
c.numeric_precision,
|
|
1069
|
+
c.numeric_scale,
|
|
1070
|
+
COALESCE(
|
|
1071
|
+
(SELECT TRUE FROM information_schema.table_constraints tc
|
|
1072
|
+
JOIN information_schema.key_column_usage kcu
|
|
1073
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1074
|
+
WHERE tc.table_name = c.table_name
|
|
1075
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
1076
|
+
AND kcu.column_name = c.column_name
|
|
1077
|
+
LIMIT 1),
|
|
1078
|
+
FALSE
|
|
1079
|
+
) AS is_primary_key,
|
|
1080
|
+
COALESCE(
|
|
1081
|
+
(SELECT TRUE FROM information_schema.table_constraints tc
|
|
1082
|
+
JOIN information_schema.key_column_usage kcu
|
|
1083
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1084
|
+
WHERE tc.table_name = c.table_name
|
|
1085
|
+
AND tc.constraint_type = 'UNIQUE'
|
|
1086
|
+
AND kcu.column_name = c.column_name
|
|
1087
|
+
LIMIT 1),
|
|
1088
|
+
FALSE
|
|
1089
|
+
) AS is_unique,
|
|
1090
|
+
pg_catalog.col_description(
|
|
1091
|
+
(SELECT oid FROM pg_catalog.pg_class WHERE relname = c.table_name),
|
|
1092
|
+
c.ordinal_position
|
|
1093
|
+
) AS column_comment
|
|
1094
|
+
FROM information_schema.columns c
|
|
1095
|
+
WHERE c.table_schema = 'public'
|
|
1096
|
+
AND c.table_name = $1
|
|
1097
|
+
ORDER BY c.ordinal_position
|
|
1098
|
+
`;
|
|
1099
|
+
const results = await querier.all(sql, [
|
|
1100
|
+
tableName
|
|
1101
|
+
]);
|
|
1102
|
+
return results.map((row)=>({
|
|
1103
|
+
name: row.column_name,
|
|
1104
|
+
type: this.normalizeType(row.data_type, row.udt_name),
|
|
1105
|
+
nullable: row.is_nullable === 'YES',
|
|
1106
|
+
defaultValue: this.parseDefaultValue(row.column_default),
|
|
1107
|
+
isPrimaryKey: row.is_primary_key,
|
|
1108
|
+
isAutoIncrement: this.isAutoIncrement(row.column_default),
|
|
1109
|
+
isUnique: row.is_unique,
|
|
1110
|
+
length: row.character_maximum_length ?? undefined,
|
|
1111
|
+
precision: row.numeric_precision ?? undefined,
|
|
1112
|
+
scale: row.numeric_scale ?? undefined,
|
|
1113
|
+
comment: row.column_comment ?? undefined
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
async getIndexes(querier, tableName) {
|
|
1117
|
+
const sql = /*sql*/ `
|
|
1118
|
+
SELECT
|
|
1119
|
+
i.relname AS index_name,
|
|
1120
|
+
array_agg(a.attname ORDER BY k.n) AS columns,
|
|
1121
|
+
ix.indisunique AS is_unique
|
|
1122
|
+
FROM pg_class t
|
|
1123
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
1124
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
1125
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
1126
|
+
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
|
|
1127
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
1128
|
+
WHERE t.relname = $1
|
|
1129
|
+
AND n.nspname = 'public'
|
|
1130
|
+
AND NOT ix.indisprimary
|
|
1131
|
+
GROUP BY i.relname, ix.indisunique
|
|
1132
|
+
ORDER BY i.relname
|
|
1133
|
+
`;
|
|
1134
|
+
const results = await querier.all(sql, [
|
|
1135
|
+
tableName
|
|
1136
|
+
]);
|
|
1137
|
+
return results.map((row)=>({
|
|
1138
|
+
name: row.index_name,
|
|
1139
|
+
columns: row.columns,
|
|
1140
|
+
unique: row.is_unique
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
async getForeignKeys(querier, tableName) {
|
|
1144
|
+
const sql = /*sql*/ `
|
|
1145
|
+
SELECT
|
|
1146
|
+
tc.constraint_name,
|
|
1147
|
+
array_agg(kcu.column_name ORDER BY kcu.ordinal_position) AS columns,
|
|
1148
|
+
ccu.table_name AS referenced_table,
|
|
1149
|
+
array_agg(ccu.column_name ORDER BY kcu.ordinal_position) AS referenced_columns,
|
|
1150
|
+
rc.delete_rule,
|
|
1151
|
+
rc.update_rule
|
|
1152
|
+
FROM information_schema.table_constraints tc
|
|
1153
|
+
JOIN information_schema.key_column_usage kcu
|
|
1154
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1155
|
+
AND tc.table_schema = kcu.table_schema
|
|
1156
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
1157
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
1158
|
+
AND ccu.table_schema = tc.table_schema
|
|
1159
|
+
JOIN information_schema.referential_constraints rc
|
|
1160
|
+
ON rc.constraint_name = tc.constraint_name
|
|
1161
|
+
AND rc.constraint_schema = tc.table_schema
|
|
1162
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
1163
|
+
AND tc.table_name = $1
|
|
1164
|
+
AND tc.table_schema = 'public'
|
|
1165
|
+
GROUP BY tc.constraint_name, ccu.table_name, rc.delete_rule, rc.update_rule
|
|
1166
|
+
ORDER BY tc.constraint_name
|
|
1167
|
+
`;
|
|
1168
|
+
const results = await querier.all(sql, [
|
|
1169
|
+
tableName
|
|
1170
|
+
]);
|
|
1171
|
+
return results.map((row)=>({
|
|
1172
|
+
name: row.constraint_name,
|
|
1173
|
+
columns: row.columns,
|
|
1174
|
+
referencedTable: row.referenced_table,
|
|
1175
|
+
referencedColumns: row.referenced_columns,
|
|
1176
|
+
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
1177
|
+
onUpdate: this.normalizeReferentialAction(row.update_rule)
|
|
1178
|
+
}));
|
|
1179
|
+
}
|
|
1180
|
+
async getPrimaryKey(querier, tableName) {
|
|
1181
|
+
const sql = /*sql*/ `
|
|
1182
|
+
SELECT kcu.column_name
|
|
1183
|
+
FROM information_schema.table_constraints tc
|
|
1184
|
+
JOIN information_schema.key_column_usage kcu
|
|
1185
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1186
|
+
AND tc.table_schema = kcu.table_schema
|
|
1187
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
1188
|
+
AND tc.table_name = $1
|
|
1189
|
+
AND tc.table_schema = 'public'
|
|
1190
|
+
ORDER BY kcu.ordinal_position
|
|
1191
|
+
`;
|
|
1192
|
+
const results = await querier.all(sql, [
|
|
1193
|
+
tableName
|
|
1194
|
+
]);
|
|
1195
|
+
if (results.length === 0) {
|
|
1196
|
+
return undefined;
|
|
1197
|
+
}
|
|
1198
|
+
return results.map((r)=>r.column_name);
|
|
1199
|
+
}
|
|
1200
|
+
normalizeType(dataType, udtName) {
|
|
1201
|
+
// Handle user-defined types and arrays
|
|
1202
|
+
if (dataType === 'USER-DEFINED') {
|
|
1203
|
+
return udtName.toUpperCase();
|
|
1204
|
+
}
|
|
1205
|
+
if (dataType === 'ARRAY') {
|
|
1206
|
+
return `${udtName.replace(/^_/, '')}[]`;
|
|
1207
|
+
}
|
|
1208
|
+
return dataType.toUpperCase();
|
|
1209
|
+
}
|
|
1210
|
+
parseDefaultValue(defaultValue) {
|
|
1211
|
+
if (!defaultValue) {
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
// Remove type casting
|
|
1215
|
+
const cleaned = defaultValue.replace(/::[a-z_]+(\[\])?/gi, '').trim();
|
|
1216
|
+
// Check for common patterns
|
|
1217
|
+
if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
1218
|
+
return cleaned.slice(1, -1);
|
|
1219
|
+
}
|
|
1220
|
+
if (cleaned === 'true' || cleaned === 'false') {
|
|
1221
|
+
return cleaned === 'true';
|
|
1222
|
+
}
|
|
1223
|
+
if (cleaned === 'NULL') {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
if (/^-?\d+$/.test(cleaned)) {
|
|
1227
|
+
return Number.parseInt(cleaned, 10);
|
|
1228
|
+
}
|
|
1229
|
+
if (/^-?\d+\.\d+$/.test(cleaned)) {
|
|
1230
|
+
return Number.parseFloat(cleaned);
|
|
1231
|
+
}
|
|
1232
|
+
// Return as-is for functions like CURRENT_TIMESTAMP, nextval(), etc.
|
|
1233
|
+
return defaultValue;
|
|
1234
|
+
}
|
|
1235
|
+
isAutoIncrement(defaultValue) {
|
|
1236
|
+
if (!defaultValue) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
return defaultValue.includes('nextval(');
|
|
1240
|
+
}
|
|
1241
|
+
normalizeReferentialAction(action) {
|
|
1242
|
+
switch(action.toUpperCase()){
|
|
1243
|
+
case 'CASCADE':
|
|
1244
|
+
return 'CASCADE';
|
|
1245
|
+
case 'SET NULL':
|
|
1246
|
+
return 'SET NULL';
|
|
1247
|
+
case 'RESTRICT':
|
|
1248
|
+
return 'RESTRICT';
|
|
1249
|
+
case 'NO ACTION':
|
|
1250
|
+
return 'NO ACTION';
|
|
1251
|
+
default:
|
|
1252
|
+
return undefined;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* SQLite schema introspector
|
|
1259
|
+
*/ class SqliteSchemaIntrospector {
|
|
1260
|
+
constructor(pool){
|
|
1261
|
+
this.pool = pool;
|
|
1262
|
+
}
|
|
1263
|
+
async getTableSchema(tableName) {
|
|
1264
|
+
const querier = await this.getQuerier();
|
|
1265
|
+
try {
|
|
1266
|
+
const exists = await this.tableExistsInternal(querier, tableName);
|
|
1267
|
+
if (!exists) {
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
const [columns, indexes, foreignKeys, primaryKey] = await Promise.all([
|
|
1271
|
+
this.getColumns(querier, tableName),
|
|
1272
|
+
this.getIndexes(querier, tableName),
|
|
1273
|
+
this.getForeignKeys(querier, tableName),
|
|
1274
|
+
this.getPrimaryKey(querier, tableName)
|
|
1275
|
+
]);
|
|
1276
|
+
return {
|
|
1277
|
+
name: tableName,
|
|
1278
|
+
columns,
|
|
1279
|
+
primaryKey,
|
|
1280
|
+
indexes,
|
|
1281
|
+
foreignKeys
|
|
1282
|
+
};
|
|
1283
|
+
} finally{
|
|
1284
|
+
await querier.release();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async getTableNames() {
|
|
1288
|
+
const querier = await this.getQuerier();
|
|
1289
|
+
try {
|
|
1290
|
+
const sql = /*sql*/ `
|
|
1291
|
+
SELECT name
|
|
1292
|
+
FROM sqlite_master
|
|
1293
|
+
WHERE type = 'table'
|
|
1294
|
+
AND name NOT LIKE 'sqlite_%'
|
|
1295
|
+
ORDER BY name
|
|
1296
|
+
`;
|
|
1297
|
+
const results = await querier.all(sql);
|
|
1298
|
+
return results.map((r)=>r.name);
|
|
1299
|
+
} finally{
|
|
1300
|
+
await querier.release();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async tableExists(tableName) {
|
|
1304
|
+
const querier = await this.getQuerier();
|
|
1305
|
+
try {
|
|
1306
|
+
return this.tableExistsInternal(querier, tableName);
|
|
1307
|
+
} finally{
|
|
1308
|
+
await querier.release();
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
async tableExistsInternal(querier, tableName) {
|
|
1312
|
+
const sql = /*sql*/ `
|
|
1313
|
+
SELECT COUNT(*) as count
|
|
1314
|
+
FROM sqlite_master
|
|
1315
|
+
WHERE type = 'table'
|
|
1316
|
+
AND name = ?
|
|
1317
|
+
`;
|
|
1318
|
+
const results = await querier.all(sql, [
|
|
1319
|
+
tableName
|
|
1320
|
+
]);
|
|
1321
|
+
return (results[0]?.count ?? 0) > 0;
|
|
1322
|
+
}
|
|
1323
|
+
async getQuerier() {
|
|
1324
|
+
const querier = await this.pool.getQuerier();
|
|
1325
|
+
if (!isSqlQuerier(querier)) {
|
|
1326
|
+
await querier.release();
|
|
1327
|
+
throw new Error('SqliteSchemaIntrospector requires a SQL-based querier');
|
|
1328
|
+
}
|
|
1329
|
+
return querier;
|
|
1330
|
+
}
|
|
1331
|
+
async getColumns(querier, tableName) {
|
|
1332
|
+
// SQLite uses PRAGMA for table info
|
|
1333
|
+
const sql = `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
1334
|
+
const results = await querier.all(sql);
|
|
1335
|
+
// Get unique columns from indexes
|
|
1336
|
+
const uniqueColumns = await this.getUniqueColumns(querier, tableName);
|
|
1337
|
+
return results.map((row)=>({
|
|
1338
|
+
name: row.name,
|
|
1339
|
+
type: this.normalizeType(row.type),
|
|
1340
|
+
nullable: row.notnull === 0,
|
|
1341
|
+
defaultValue: this.parseDefaultValue(row.dflt_value),
|
|
1342
|
+
isPrimaryKey: row.pk > 0,
|
|
1343
|
+
isAutoIncrement: row.pk > 0 && row.type.toUpperCase() === 'INTEGER',
|
|
1344
|
+
isUnique: uniqueColumns.has(row.name),
|
|
1345
|
+
length: this.extractLength(row.type),
|
|
1346
|
+
precision: undefined,
|
|
1347
|
+
scale: undefined,
|
|
1348
|
+
comment: undefined
|
|
1349
|
+
}));
|
|
1350
|
+
}
|
|
1351
|
+
async getUniqueColumns(querier, tableName) {
|
|
1352
|
+
const sql = `PRAGMA index_list(${this.escapeId(tableName)})`;
|
|
1353
|
+
const indexes = await querier.all(sql);
|
|
1354
|
+
const uniqueColumns = new Set();
|
|
1355
|
+
for (const index of indexes){
|
|
1356
|
+
if (index.unique && index.origin === 'u') {
|
|
1357
|
+
const indexInfo = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
|
|
1358
|
+
// Only single-column unique constraints
|
|
1359
|
+
if (indexInfo.length === 1) {
|
|
1360
|
+
uniqueColumns.add(indexInfo[0].name);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return uniqueColumns;
|
|
1365
|
+
}
|
|
1366
|
+
async getIndexes(querier, tableName) {
|
|
1367
|
+
const sql = `PRAGMA index_list(${this.escapeId(tableName)})`;
|
|
1368
|
+
const indexes = await querier.all(sql);
|
|
1369
|
+
const result = [];
|
|
1370
|
+
for (const index of indexes){
|
|
1371
|
+
// Skip auto-generated indexes (primary key, unique constraints)
|
|
1372
|
+
if (index.origin !== 'c') {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const columns = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
|
|
1376
|
+
result.push({
|
|
1377
|
+
name: index.name,
|
|
1378
|
+
columns: columns.map((c)=>c.name),
|
|
1379
|
+
unique: Boolean(index.unique)
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
return result;
|
|
1383
|
+
}
|
|
1384
|
+
async getForeignKeys(querier, tableName) {
|
|
1385
|
+
const sql = `PRAGMA foreign_key_list(${this.escapeId(tableName)})`;
|
|
1386
|
+
const results = await querier.all(sql);
|
|
1387
|
+
// Group by id to handle composite foreign keys
|
|
1388
|
+
const grouped = new Map();
|
|
1389
|
+
for (const row of results){
|
|
1390
|
+
const existing = grouped.get(row.id) ?? [];
|
|
1391
|
+
existing.push(row);
|
|
1392
|
+
grouped.set(row.id, existing);
|
|
1393
|
+
}
|
|
1394
|
+
return Array.from(grouped.entries()).map(([id, rows])=>{
|
|
1395
|
+
const first = rows[0];
|
|
1396
|
+
return {
|
|
1397
|
+
name: `fk_${tableName}_${id}`,
|
|
1398
|
+
columns: rows.map((r)=>r.from),
|
|
1399
|
+
referencedTable: first.table,
|
|
1400
|
+
referencedColumns: rows.map((r)=>r.to),
|
|
1401
|
+
onDelete: this.normalizeReferentialAction(first.on_delete),
|
|
1402
|
+
onUpdate: this.normalizeReferentialAction(first.on_update)
|
|
1403
|
+
};
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
async getPrimaryKey(querier, tableName) {
|
|
1407
|
+
const sql = `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
1408
|
+
const results = await querier.all(sql);
|
|
1409
|
+
const pkColumns = results.filter((r)=>r.pk > 0).sort((a, b)=>a.pk - b.pk);
|
|
1410
|
+
if (pkColumns.length === 0) {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
return pkColumns.map((r)=>r.name);
|
|
1414
|
+
}
|
|
1415
|
+
escapeId(identifier) {
|
|
1416
|
+
return `\`${identifier.replace(/`/g, '``')}\``;
|
|
1417
|
+
}
|
|
1418
|
+
normalizeType(type) {
|
|
1419
|
+
// Extract base type without length/precision
|
|
1420
|
+
const match = type.match(/^([A-Za-z]+)/);
|
|
1421
|
+
return match ? match[1].toUpperCase() : type.toUpperCase();
|
|
1422
|
+
}
|
|
1423
|
+
extractLength(type) {
|
|
1424
|
+
const match = type.match(/\((\d+)\)/);
|
|
1425
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
1426
|
+
}
|
|
1427
|
+
parseDefaultValue(defaultValue) {
|
|
1428
|
+
if (defaultValue === null) {
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
// Check for common patterns
|
|
1432
|
+
if (defaultValue === 'NULL') {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
if (defaultValue === 'CURRENT_TIMESTAMP' || defaultValue === 'CURRENT_DATE' || defaultValue === 'CURRENT_TIME') {
|
|
1436
|
+
return defaultValue;
|
|
1437
|
+
}
|
|
1438
|
+
if (/^'.*'$/.test(defaultValue)) {
|
|
1439
|
+
return defaultValue.slice(1, -1);
|
|
1440
|
+
}
|
|
1441
|
+
if (/^-?\d+$/.test(defaultValue)) {
|
|
1442
|
+
return Number.parseInt(defaultValue, 10);
|
|
1443
|
+
}
|
|
1444
|
+
if (/^-?\d+\.\d+$/.test(defaultValue)) {
|
|
1445
|
+
return Number.parseFloat(defaultValue);
|
|
1446
|
+
}
|
|
1447
|
+
return defaultValue;
|
|
1448
|
+
}
|
|
1449
|
+
normalizeReferentialAction(action) {
|
|
1450
|
+
switch(action.toUpperCase()){
|
|
1451
|
+
case 'CASCADE':
|
|
1452
|
+
return 'CASCADE';
|
|
1453
|
+
case 'SET NULL':
|
|
1454
|
+
return 'SET NULL';
|
|
1455
|
+
case 'RESTRICT':
|
|
1456
|
+
return 'RESTRICT';
|
|
1457
|
+
case 'NO ACTION':
|
|
1458
|
+
return 'NO ACTION';
|
|
1459
|
+
default:
|
|
1460
|
+
return undefined;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
class MongoSchemaGenerator extends AbstractDialect {
|
|
1466
|
+
generateCreateTable(entity) {
|
|
1467
|
+
const meta = getMeta(entity);
|
|
1468
|
+
const collectionName = this.resolveTableName(entity, meta);
|
|
1469
|
+
const indexes = [];
|
|
1470
|
+
for(const key in meta.fields){
|
|
1471
|
+
const field = meta.fields[key];
|
|
1472
|
+
if (field.index) {
|
|
1473
|
+
const columnName = this.resolveColumnName(key, field);
|
|
1474
|
+
const indexName = typeof field.index === 'string' ? field.index : `idx_${collectionName}_${columnName}`;
|
|
1475
|
+
indexes.push({
|
|
1476
|
+
name: indexName,
|
|
1477
|
+
columns: [
|
|
1478
|
+
columnName
|
|
1479
|
+
],
|
|
1480
|
+
unique: !!field.unique
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return JSON.stringify({
|
|
1485
|
+
action: 'createCollection',
|
|
1486
|
+
name: collectionName,
|
|
1487
|
+
indexes
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
generateDropTable(entity) {
|
|
1491
|
+
const meta = getMeta(entity);
|
|
1492
|
+
const collectionName = this.resolveTableName(entity, meta);
|
|
1493
|
+
return JSON.stringify({
|
|
1494
|
+
action: 'dropCollection',
|
|
1495
|
+
name: collectionName
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
generateAlterTable(diff) {
|
|
1499
|
+
const statements = [];
|
|
1500
|
+
if (diff.indexesToAdd?.length) {
|
|
1501
|
+
for (const index of diff.indexesToAdd){
|
|
1502
|
+
statements.push(this.generateCreateIndex(diff.tableName, index));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return statements;
|
|
1506
|
+
}
|
|
1507
|
+
generateAlterTableDown(diff) {
|
|
1508
|
+
const statements = [];
|
|
1509
|
+
if (diff.indexesToAdd?.length) {
|
|
1510
|
+
for (const index of diff.indexesToAdd){
|
|
1511
|
+
statements.push(this.generateDropIndex(diff.tableName, index.name));
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return statements;
|
|
1515
|
+
}
|
|
1516
|
+
generateCreateIndex(tableName, index) {
|
|
1517
|
+
const key = {};
|
|
1518
|
+
for (const col of index.columns){
|
|
1519
|
+
key[col] = 1;
|
|
1520
|
+
}
|
|
1521
|
+
return JSON.stringify({
|
|
1522
|
+
action: 'createIndex',
|
|
1523
|
+
collection: tableName,
|
|
1524
|
+
name: index.name,
|
|
1525
|
+
key,
|
|
1526
|
+
options: {
|
|
1527
|
+
unique: index.unique,
|
|
1528
|
+
name: index.name
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
generateDropIndex(tableName, indexName) {
|
|
1533
|
+
return JSON.stringify({
|
|
1534
|
+
action: 'dropIndex',
|
|
1535
|
+
collection: tableName,
|
|
1536
|
+
name: indexName
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
getSqlType() {
|
|
1540
|
+
return '';
|
|
1541
|
+
}
|
|
1542
|
+
diffSchema(entity, currentSchema) {
|
|
1543
|
+
const meta = getMeta(entity);
|
|
1544
|
+
const collectionName = this.resolveTableName(entity, meta);
|
|
1545
|
+
if (!currentSchema) {
|
|
1546
|
+
return {
|
|
1547
|
+
tableName: collectionName,
|
|
1548
|
+
type: 'create'
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const indexesToAdd = [];
|
|
1552
|
+
const existingIndexes = new Set(currentSchema.indexes?.map((i)=>i.name) ?? []);
|
|
1553
|
+
for(const key in meta.fields){
|
|
1554
|
+
const field = meta.fields[key];
|
|
1555
|
+
if (field.index) {
|
|
1556
|
+
const columnName = this.resolveColumnName(key, field);
|
|
1557
|
+
const indexName = typeof field.index === 'string' ? field.index : `idx_${collectionName}_${columnName}`;
|
|
1558
|
+
if (!existingIndexes.has(indexName)) {
|
|
1559
|
+
indexesToAdd.push({
|
|
1560
|
+
name: indexName,
|
|
1561
|
+
columns: [
|
|
1562
|
+
columnName
|
|
1563
|
+
],
|
|
1564
|
+
unique: !!field.unique
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (indexesToAdd.length === 0) {
|
|
1570
|
+
return undefined;
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
tableName: collectionName,
|
|
1574
|
+
type: 'alter',
|
|
1575
|
+
indexesToAdd
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
class MongoSchemaIntrospector {
|
|
1581
|
+
constructor(pool){
|
|
1582
|
+
this.pool = pool;
|
|
1583
|
+
}
|
|
1584
|
+
async getTableSchema(tableName) {
|
|
1585
|
+
const querier = await this.pool.getQuerier();
|
|
1586
|
+
try {
|
|
1587
|
+
const { db } = querier;
|
|
1588
|
+
const collections = await db.listCollections({
|
|
1589
|
+
name: tableName
|
|
1590
|
+
}).toArray();
|
|
1591
|
+
if (collections.length === 0) {
|
|
1592
|
+
return undefined;
|
|
1593
|
+
}
|
|
1594
|
+
// MongoDB doesn't have a fixed schema, but we can look at the indexes
|
|
1595
|
+
const indexes = await db.collection(tableName).indexes();
|
|
1596
|
+
return {
|
|
1597
|
+
name: tableName,
|
|
1598
|
+
columns: [],
|
|
1599
|
+
indexes: indexes.map((idx)=>({
|
|
1600
|
+
name: idx.name,
|
|
1601
|
+
columns: Object.keys(idx.key),
|
|
1602
|
+
unique: !!idx.unique
|
|
1603
|
+
}))
|
|
1604
|
+
};
|
|
1605
|
+
} finally{
|
|
1606
|
+
await querier.release();
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async getTableNames() {
|
|
1610
|
+
const querier = await this.pool.getQuerier();
|
|
1611
|
+
try {
|
|
1612
|
+
const { db } = querier;
|
|
1613
|
+
const collections = await db.listCollections().toArray();
|
|
1614
|
+
return collections.map((c)=>c.name);
|
|
1615
|
+
} finally{
|
|
1616
|
+
await querier.release();
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
async tableExists(tableName) {
|
|
1620
|
+
const names = await this.getTableNames();
|
|
1621
|
+
return names.includes(tableName);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Stores migration state in a database table.
|
|
1627
|
+
* Uses the querier's dialect for escaping and placeholders.
|
|
1628
|
+
*/ class DatabaseMigrationStorage {
|
|
1629
|
+
constructor(pool, options = {}){
|
|
1630
|
+
this.pool = pool;
|
|
1631
|
+
this.storageInitialized = false;
|
|
1632
|
+
this.tableName = options.tableName ?? 'uql_migrations';
|
|
1633
|
+
}
|
|
1634
|
+
async ensureStorage() {
|
|
1635
|
+
if (this.storageInitialized) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const querier = await this.pool.getQuerier();
|
|
1639
|
+
if (!isSqlQuerier(querier)) {
|
|
1640
|
+
await querier.release();
|
|
1641
|
+
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
1642
|
+
}
|
|
1643
|
+
try {
|
|
1644
|
+
await this.createTableIfNotExists(querier);
|
|
1645
|
+
this.storageInitialized = true;
|
|
1646
|
+
} finally{
|
|
1647
|
+
await querier.release();
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
async createTableIfNotExists(querier) {
|
|
1651
|
+
const { escapeId } = querier.dialect;
|
|
1652
|
+
const sql = `
|
|
1653
|
+
CREATE TABLE IF NOT EXISTS ${escapeId(this.tableName)} (
|
|
1654
|
+
${escapeId('name')} VARCHAR(255) PRIMARY KEY,
|
|
1655
|
+
${escapeId('executed_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
1656
|
+
)
|
|
1657
|
+
`;
|
|
1658
|
+
await querier.run(sql);
|
|
1659
|
+
}
|
|
1660
|
+
async executed() {
|
|
1661
|
+
await this.ensureStorage();
|
|
1662
|
+
const querier = await this.pool.getQuerier();
|
|
1663
|
+
if (!isSqlQuerier(querier)) {
|
|
1664
|
+
await querier.release();
|
|
1665
|
+
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
1666
|
+
}
|
|
1667
|
+
try {
|
|
1668
|
+
const { escapeId } = querier.dialect;
|
|
1669
|
+
const sql = `SELECT ${escapeId('name')} FROM ${escapeId(this.tableName)} ORDER BY ${escapeId('name')} ASC`;
|
|
1670
|
+
const results = await querier.all(sql);
|
|
1671
|
+
return results.map((r)=>r.name);
|
|
1672
|
+
} finally{
|
|
1673
|
+
await querier.release();
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Log a migration as executed - uses provided querier (within transaction)
|
|
1678
|
+
*/ async logWithQuerier(querier, migrationName) {
|
|
1679
|
+
await this.ensureStorage();
|
|
1680
|
+
const { escapeId, placeholder } = querier.dialect;
|
|
1681
|
+
const sql = `INSERT INTO ${escapeId(this.tableName)} (${escapeId('name')}) VALUES (${placeholder(1)})`;
|
|
1682
|
+
await querier.run(sql, [
|
|
1683
|
+
migrationName
|
|
1684
|
+
]);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Unlog a migration - uses provided querier (within transaction)
|
|
1688
|
+
*/ async unlogWithQuerier(querier, migrationName) {
|
|
1689
|
+
await this.ensureStorage();
|
|
1690
|
+
const { escapeId, placeholder } = querier.dialect;
|
|
1691
|
+
const sql = `DELETE FROM ${escapeId(this.tableName)} WHERE ${escapeId('name')} = ${placeholder(1)}`;
|
|
1692
|
+
await querier.run(sql, [
|
|
1693
|
+
migrationName
|
|
1694
|
+
]);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Main class for managing database migrations
|
|
1700
|
+
*/ class Migrator {
|
|
1701
|
+
constructor(pool, options = {}){
|
|
1702
|
+
this.pool = pool;
|
|
1703
|
+
this.dialect = options.dialect ?? pool.dialect ?? 'postgres';
|
|
1704
|
+
this.storage = options.storage ?? new DatabaseMigrationStorage(pool, {
|
|
1705
|
+
tableName: options.tableName
|
|
1706
|
+
});
|
|
1707
|
+
this.migrationsPath = options.migrationsPath ?? './migrations';
|
|
1708
|
+
this.logger = options.logger ?? (()=>{});
|
|
1709
|
+
this.entities = options.entities ?? [];
|
|
1710
|
+
this.schemaIntrospector = this.createIntrospector();
|
|
1711
|
+
this.schemaGenerator = options.schemaGenerator ?? this.createGenerator(options.namingStrategy);
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Set the schema generator for DDL operations
|
|
1715
|
+
*/ setSchemaGenerator(generator) {
|
|
1716
|
+
this.schemaGenerator = generator;
|
|
1717
|
+
}
|
|
1718
|
+
createIntrospector() {
|
|
1719
|
+
switch(this.dialect){
|
|
1720
|
+
case 'postgres':
|
|
1721
|
+
return new PostgresSchemaIntrospector(this.pool);
|
|
1722
|
+
case 'mysql':
|
|
1723
|
+
return new MysqlSchemaIntrospector(this.pool);
|
|
1724
|
+
case 'mariadb':
|
|
1725
|
+
return new MariadbSchemaIntrospector(this.pool);
|
|
1726
|
+
case 'sqlite':
|
|
1727
|
+
return new SqliteSchemaIntrospector(this.pool);
|
|
1728
|
+
case 'mongodb':
|
|
1729
|
+
return new MongoSchemaIntrospector(this.pool);
|
|
1730
|
+
default:
|
|
1731
|
+
return undefined;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
createGenerator(namingStrategy) {
|
|
1735
|
+
switch(this.dialect){
|
|
1736
|
+
case 'postgres':
|
|
1737
|
+
return new PostgresSchemaGenerator(namingStrategy);
|
|
1738
|
+
case 'mysql':
|
|
1739
|
+
return new MysqlSchemaGenerator(namingStrategy);
|
|
1740
|
+
case 'mariadb':
|
|
1741
|
+
return new MysqlSchemaGenerator(namingStrategy);
|
|
1742
|
+
case 'sqlite':
|
|
1743
|
+
return new SqliteSchemaGenerator(namingStrategy);
|
|
1744
|
+
case 'mongodb':
|
|
1745
|
+
return new MongoSchemaGenerator(namingStrategy);
|
|
1746
|
+
default:
|
|
1747
|
+
return undefined;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Get the SQL dialect
|
|
1752
|
+
*/ getDialect() {
|
|
1753
|
+
return this.dialect;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Get all discovered migrations from the migrations directory
|
|
1757
|
+
*/ async getMigrations() {
|
|
1758
|
+
const files = await this.getMigrationFiles();
|
|
1759
|
+
const migrations = [];
|
|
1760
|
+
for (const file of files){
|
|
1761
|
+
const migration = await this.loadMigration(file);
|
|
1762
|
+
if (migration) {
|
|
1763
|
+
migrations.push(migration);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
// Sort by name (which typically includes timestamp)
|
|
1767
|
+
return migrations.sort((a, b)=>a.name.localeCompare(b.name));
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Get list of pending migrations (not yet executed)
|
|
1771
|
+
*/ async pending() {
|
|
1772
|
+
const [migrations, executed] = await Promise.all([
|
|
1773
|
+
this.getMigrations(),
|
|
1774
|
+
this.storage.executed()
|
|
1775
|
+
]);
|
|
1776
|
+
const executedSet = new Set(executed);
|
|
1777
|
+
return migrations.filter((m)=>!executedSet.has(m.name));
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Get list of executed migrations
|
|
1781
|
+
*/ async executed() {
|
|
1782
|
+
return this.storage.executed();
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Run all pending migrations
|
|
1786
|
+
*/ async up(options = {}) {
|
|
1787
|
+
const pendingMigrations = await this.pending();
|
|
1788
|
+
const results = [];
|
|
1789
|
+
let migrationsToRun = pendingMigrations;
|
|
1790
|
+
if (options.to) {
|
|
1791
|
+
const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
|
|
1792
|
+
if (toIndex === -1) {
|
|
1793
|
+
throw new Error(`Migration '${options.to}' not found`);
|
|
1794
|
+
}
|
|
1795
|
+
migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
|
|
1796
|
+
}
|
|
1797
|
+
if (options.step !== undefined) {
|
|
1798
|
+
migrationsToRun = migrationsToRun.slice(0, options.step);
|
|
1799
|
+
}
|
|
1800
|
+
for (const migration of migrationsToRun){
|
|
1801
|
+
const result = await this.runMigration(migration, 'up');
|
|
1802
|
+
results.push(result);
|
|
1803
|
+
if (!result.success) {
|
|
1804
|
+
break; // Stop on first failure
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return results;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Rollback migrations
|
|
1811
|
+
*/ async down(options = {}) {
|
|
1812
|
+
const [migrations, executed] = await Promise.all([
|
|
1813
|
+
this.getMigrations(),
|
|
1814
|
+
this.storage.executed()
|
|
1815
|
+
]);
|
|
1816
|
+
const executedSet = new Set(executed);
|
|
1817
|
+
const executedMigrations = migrations.filter((m)=>executedSet.has(m.name)).reverse(); // Rollback in reverse order
|
|
1818
|
+
const results = [];
|
|
1819
|
+
let migrationsToRun = executedMigrations;
|
|
1820
|
+
if (options.to) {
|
|
1821
|
+
const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
|
|
1822
|
+
if (toIndex === -1) {
|
|
1823
|
+
throw new Error(`Migration '${options.to}' not found`);
|
|
1824
|
+
}
|
|
1825
|
+
migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
|
|
1826
|
+
}
|
|
1827
|
+
if (options.step !== undefined) {
|
|
1828
|
+
migrationsToRun = migrationsToRun.slice(0, options.step);
|
|
1829
|
+
}
|
|
1830
|
+
for (const migration of migrationsToRun){
|
|
1831
|
+
const result = await this.runMigration(migration, 'down');
|
|
1832
|
+
results.push(result);
|
|
1833
|
+
if (!result.success) {
|
|
1834
|
+
break; // Stop on first failure
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return results;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Run a single migration within a transaction
|
|
1841
|
+
*/ async runMigration(migration, direction) {
|
|
1842
|
+
const startTime = Date.now();
|
|
1843
|
+
const querier = await this.pool.getQuerier();
|
|
1844
|
+
if (!isSqlQuerier(querier)) {
|
|
1845
|
+
await querier.release();
|
|
1846
|
+
throw new Error('Migrator requires a SQL-based querier');
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
this.logger(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
|
|
1850
|
+
await querier.beginTransaction();
|
|
1851
|
+
if (direction === 'up') {
|
|
1852
|
+
await migration.up(querier);
|
|
1853
|
+
// Log within the same transaction
|
|
1854
|
+
await this.storage.logWithQuerier(querier, migration.name);
|
|
1855
|
+
} else {
|
|
1856
|
+
await migration.down(querier);
|
|
1857
|
+
// Unlog within the same transaction
|
|
1858
|
+
await this.storage.unlogWithQuerier(querier, migration.name);
|
|
1859
|
+
}
|
|
1860
|
+
await querier.commitTransaction();
|
|
1861
|
+
const duration = Date.now() - startTime;
|
|
1862
|
+
this.logger(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
|
|
1863
|
+
return {
|
|
1864
|
+
name: migration.name,
|
|
1865
|
+
direction,
|
|
1866
|
+
duration,
|
|
1867
|
+
success: true
|
|
1868
|
+
};
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
await querier.rollbackTransaction();
|
|
1871
|
+
const duration = Date.now() - startTime;
|
|
1872
|
+
this.logger(`Migration ${migration.name} failed: ${error.message}`);
|
|
1873
|
+
return {
|
|
1874
|
+
name: migration.name,
|
|
1875
|
+
direction,
|
|
1876
|
+
duration,
|
|
1877
|
+
success: false,
|
|
1878
|
+
error: error
|
|
1879
|
+
};
|
|
1880
|
+
} finally{
|
|
1881
|
+
await querier.release();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Generate a new migration file
|
|
1886
|
+
*/ async generate(name) {
|
|
1887
|
+
const timestamp = this.getTimestamp();
|
|
1888
|
+
const fileName = `${timestamp}_${this.slugify(name)}.ts`;
|
|
1889
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
1890
|
+
const content = this.generateMigrationContent(name);
|
|
1891
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
1892
|
+
await mkdir(this.migrationsPath, {
|
|
1893
|
+
recursive: true
|
|
1894
|
+
});
|
|
1895
|
+
await writeFile(filePath, content, 'utf-8');
|
|
1896
|
+
this.logger(`Created migration: ${filePath}`);
|
|
1897
|
+
return filePath;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Generate a migration based on entity schema differences
|
|
1901
|
+
*/ async generateFromEntities(name) {
|
|
1902
|
+
if (!this.schemaGenerator) {
|
|
1903
|
+
throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
|
|
1904
|
+
}
|
|
1905
|
+
const diffs = await this.getDiffs();
|
|
1906
|
+
const upStatements = [];
|
|
1907
|
+
const downStatements = [];
|
|
1908
|
+
for (const diff of diffs){
|
|
1909
|
+
if (diff.type === 'create') {
|
|
1910
|
+
const entity = this.findEntityForTable(diff.tableName);
|
|
1911
|
+
if (entity) {
|
|
1912
|
+
upStatements.push(this.schemaGenerator.generateCreateTable(entity));
|
|
1913
|
+
downStatements.push(this.schemaGenerator.generateDropTable(entity));
|
|
1914
|
+
}
|
|
1915
|
+
} else if (diff.type === 'alter') {
|
|
1916
|
+
const alterStatements = this.schemaGenerator.generateAlterTable(diff);
|
|
1917
|
+
upStatements.push(...alterStatements);
|
|
1918
|
+
const alterDownStatements = this.schemaGenerator.generateAlterTableDown(diff);
|
|
1919
|
+
downStatements.push(...alterDownStatements);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (upStatements.length === 0) {
|
|
1923
|
+
this.logger('No schema changes detected.');
|
|
1924
|
+
return '';
|
|
1925
|
+
}
|
|
1926
|
+
const timestamp = this.getTimestamp();
|
|
1927
|
+
const fileName = `${timestamp}_${this.slugify(name)}.ts`;
|
|
1928
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
1929
|
+
const content = this.generateMigrationContentWithStatements(name, upStatements, downStatements.reverse());
|
|
1930
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
1931
|
+
await mkdir(this.migrationsPath, {
|
|
1932
|
+
recursive: true
|
|
1933
|
+
});
|
|
1934
|
+
await writeFile(filePath, content, 'utf-8');
|
|
1935
|
+
this.logger(`Created migration from entities: ${filePath}`);
|
|
1936
|
+
return filePath;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Get all schema differences between entities and database
|
|
1940
|
+
*/ async getDiffs() {
|
|
1941
|
+
if (!this.schemaGenerator || !this.schemaIntrospector) {
|
|
1942
|
+
throw new Error('Schema generator and introspector must be set');
|
|
1943
|
+
}
|
|
1944
|
+
const entities = this.entities.length > 0 ? this.entities : getEntities();
|
|
1945
|
+
const diffs = [];
|
|
1946
|
+
for (const entity of entities){
|
|
1947
|
+
const meta = getMeta(entity);
|
|
1948
|
+
const tableName = this.schemaGenerator.resolveTableName(entity, meta);
|
|
1949
|
+
const currentSchema = await this.schemaIntrospector.getTableSchema(tableName);
|
|
1950
|
+
const diff = this.schemaGenerator.diffSchema(entity, currentSchema);
|
|
1951
|
+
if (diff) {
|
|
1952
|
+
diffs.push(diff);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return diffs;
|
|
1956
|
+
}
|
|
1957
|
+
findEntityForTable(tableName) {
|
|
1958
|
+
const entities = this.entities.length > 0 ? this.entities : getEntities();
|
|
1959
|
+
for (const entity of entities){
|
|
1960
|
+
const meta = getMeta(entity);
|
|
1961
|
+
const name = this.schemaGenerator.resolveTableName(entity, meta);
|
|
1962
|
+
if (name === tableName) {
|
|
1963
|
+
return entity;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return undefined;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Sync schema directly (for development only - not for production!)
|
|
1970
|
+
*/ async sync(options = {}) {
|
|
1971
|
+
if (options.force) {
|
|
1972
|
+
return this.syncForce();
|
|
1973
|
+
}
|
|
1974
|
+
return this.autoSync({
|
|
1975
|
+
safe: true
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Drops and recreates all tables (Development only!)
|
|
1980
|
+
*/ async syncForce() {
|
|
1981
|
+
if (!this.schemaGenerator) {
|
|
1982
|
+
throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
|
|
1983
|
+
}
|
|
1984
|
+
const entities = this.entities.length > 0 ? this.entities : getEntities();
|
|
1985
|
+
const querier = await this.pool.getQuerier();
|
|
1986
|
+
if (!isSqlQuerier(querier)) {
|
|
1987
|
+
await querier.release();
|
|
1988
|
+
throw new Error('Migrator requires a SQL-based querier');
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
await querier.beginTransaction();
|
|
1992
|
+
// Drop all tables first (in reverse order for foreign keys)
|
|
1993
|
+
for (const entity of [
|
|
1994
|
+
...entities
|
|
1995
|
+
].reverse()){
|
|
1996
|
+
const dropSql = this.schemaGenerator.generateDropTable(entity);
|
|
1997
|
+
this.logger(`Executing: ${dropSql}`);
|
|
1998
|
+
await querier.run(dropSql);
|
|
1999
|
+
}
|
|
2000
|
+
// Create all tables
|
|
2001
|
+
for (const entity of entities){
|
|
2002
|
+
const createSql = this.schemaGenerator.generateCreateTable(entity);
|
|
2003
|
+
this.logger(`Executing: ${createSql}`);
|
|
2004
|
+
await querier.run(createSql);
|
|
2005
|
+
}
|
|
2006
|
+
await querier.commitTransaction();
|
|
2007
|
+
this.logger('Schema sync (force) completed');
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
await querier.rollbackTransaction();
|
|
2010
|
+
throw error;
|
|
2011
|
+
} finally{
|
|
2012
|
+
await querier.release();
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Safely synchronizes the schema by only adding missing tables and columns.
|
|
2017
|
+
*/ async autoSync(options = {}) {
|
|
2018
|
+
if (!this.schemaGenerator || !this.schemaIntrospector) {
|
|
2019
|
+
throw new Error('Schema generator and introspector must be set');
|
|
2020
|
+
}
|
|
2021
|
+
const diffs = await this.getDiffs();
|
|
2022
|
+
const statements = [];
|
|
2023
|
+
for (const diff of diffs){
|
|
2024
|
+
if (diff.type === 'create') {
|
|
2025
|
+
const entity = this.findEntityForTable(diff.tableName);
|
|
2026
|
+
if (entity) {
|
|
2027
|
+
statements.push(this.schemaGenerator.generateCreateTable(entity));
|
|
2028
|
+
}
|
|
2029
|
+
} else if (diff.type === 'alter') {
|
|
2030
|
+
const filteredDiff = this.filterDiff(diff, options);
|
|
2031
|
+
const alterStatements = this.schemaGenerator.generateAlterTable(filteredDiff);
|
|
2032
|
+
statements.push(...alterStatements);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (statements.length === 0) {
|
|
2036
|
+
if (options.logging) this.logger('Schema is already in sync.');
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
await this.executeSyncStatements(statements, options);
|
|
2040
|
+
}
|
|
2041
|
+
filterDiff(diff, options) {
|
|
2042
|
+
const filteredDiff = {
|
|
2043
|
+
...diff
|
|
2044
|
+
};
|
|
2045
|
+
if (options.safe !== false) {
|
|
2046
|
+
// In safe mode, we only allow additions
|
|
2047
|
+
delete filteredDiff.columnsToDrop;
|
|
2048
|
+
delete filteredDiff.indexesToDrop;
|
|
2049
|
+
delete filteredDiff.foreignKeysToDrop;
|
|
2050
|
+
}
|
|
2051
|
+
if (!options.drop) {
|
|
2052
|
+
delete filteredDiff.columnsToDrop;
|
|
2053
|
+
}
|
|
2054
|
+
return filteredDiff;
|
|
2055
|
+
}
|
|
2056
|
+
async executeSyncStatements(statements, options) {
|
|
2057
|
+
const querier = await this.pool.getQuerier();
|
|
2058
|
+
try {
|
|
2059
|
+
if (this.dialect === 'mongodb') {
|
|
2060
|
+
await this.executeMongoSyncStatements(statements, options, querier);
|
|
2061
|
+
} else {
|
|
2062
|
+
await this.executeSqlSyncStatements(statements, options, querier);
|
|
2063
|
+
}
|
|
2064
|
+
if (options.logging) this.logger('Schema synchronization completed');
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (this.dialect !== 'mongodb' && isSqlQuerier(querier)) {
|
|
2067
|
+
await querier.rollbackTransaction();
|
|
2068
|
+
}
|
|
2069
|
+
throw error;
|
|
2070
|
+
} finally{
|
|
2071
|
+
await querier.release();
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
async executeMongoSyncStatements(statements, options, querier) {
|
|
2075
|
+
const db = querier.db;
|
|
2076
|
+
for (const stmt of statements){
|
|
2077
|
+
const cmd = JSON.parse(stmt);
|
|
2078
|
+
if (options.logging) this.logger(`Executing MongoDB: ${stmt}`);
|
|
2079
|
+
const collectionName = cmd.name || cmd.collection;
|
|
2080
|
+
if (!collectionName) {
|
|
2081
|
+
throw new Error(`MongoDB command missing collection name: ${stmt}`);
|
|
2082
|
+
}
|
|
2083
|
+
const collection = db.collection(collectionName);
|
|
2084
|
+
if (cmd.action === 'createCollection') {
|
|
2085
|
+
await db.createCollection(cmd.name);
|
|
2086
|
+
if (cmd.indexes?.length) {
|
|
2087
|
+
for (const idx of cmd.indexes){
|
|
2088
|
+
const key = Object.fromEntries(idx.columns.map((c)=>[
|
|
2089
|
+
c,
|
|
2090
|
+
1
|
|
2091
|
+
]));
|
|
2092
|
+
await collection.createIndex(key, {
|
|
2093
|
+
unique: idx.unique,
|
|
2094
|
+
name: idx.name
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
} else if (cmd.action === 'dropCollection') {
|
|
2099
|
+
await collection.drop();
|
|
2100
|
+
} else if (cmd.action === 'createIndex') {
|
|
2101
|
+
await collection.createIndex(cmd.key, cmd.options);
|
|
2102
|
+
} else if (cmd.action === 'dropIndex') {
|
|
2103
|
+
await collection.dropIndex(cmd.name);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
async executeSqlSyncStatements(statements, options, querier) {
|
|
2108
|
+
if (!isSqlQuerier(querier)) {
|
|
2109
|
+
throw new Error('Migrator requires a SQL-based querier for this dialect');
|
|
2110
|
+
}
|
|
2111
|
+
await querier.beginTransaction();
|
|
2112
|
+
for (const sql of statements){
|
|
2113
|
+
if (options.logging) this.logger(`Executing: ${sql}`);
|
|
2114
|
+
await querier.run(sql);
|
|
2115
|
+
}
|
|
2116
|
+
await querier.commitTransaction();
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Get migration status
|
|
2120
|
+
*/ async status() {
|
|
2121
|
+
const [pending, executed] = await Promise.all([
|
|
2122
|
+
this.pending().then((m)=>m.map((x)=>x.name)),
|
|
2123
|
+
this.executed()
|
|
2124
|
+
]);
|
|
2125
|
+
return {
|
|
2126
|
+
pending,
|
|
2127
|
+
executed
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Get migration files from the migrations directory
|
|
2132
|
+
*/ async getMigrationFiles() {
|
|
2133
|
+
try {
|
|
2134
|
+
const files = await readdir(this.migrationsPath);
|
|
2135
|
+
return files.filter((f)=>/\.(ts|js|mjs)$/.test(f)).filter((f)=>!f.endsWith('.d.ts')).sort();
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
if (error.code === 'ENOENT') {
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2140
|
+
throw error;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Load a migration from a file
|
|
2145
|
+
*/ async loadMigration(fileName) {
|
|
2146
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
2147
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
2148
|
+
try {
|
|
2149
|
+
const module = await import(fileUrl);
|
|
2150
|
+
const migration = module.default ?? module;
|
|
2151
|
+
if (this.isMigration(migration)) {
|
|
2152
|
+
return {
|
|
2153
|
+
name: this.getMigrationName(fileName),
|
|
2154
|
+
up: migration.up.bind(migration),
|
|
2155
|
+
down: migration.down.bind(migration)
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
this.logger(`Warning: ${fileName} is not a valid migration`);
|
|
2159
|
+
return undefined;
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
this.logger(`Error loading migration ${fileName}: ${error.message}`);
|
|
2162
|
+
return undefined;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Check if an object is a valid migration
|
|
2167
|
+
*/ isMigration(obj) {
|
|
2168
|
+
return typeof obj === 'object' && obj !== undefined && obj !== null && typeof obj.up === 'function' && typeof obj.down === 'function';
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Extract migration name from filename
|
|
2172
|
+
*/ getMigrationName(fileName) {
|
|
2173
|
+
return basename(fileName, extname(fileName));
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Generate timestamp string for migration names
|
|
2177
|
+
*/ getTimestamp() {
|
|
2178
|
+
const now = new Date();
|
|
2179
|
+
return [
|
|
2180
|
+
now.getFullYear(),
|
|
2181
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
2182
|
+
String(now.getDate()).padStart(2, '0'),
|
|
2183
|
+
String(now.getHours()).padStart(2, '0'),
|
|
2184
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
2185
|
+
String(now.getSeconds()).padStart(2, '0')
|
|
2186
|
+
].join('');
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Convert a string to a slug for filenames
|
|
2190
|
+
*/ slugify(text) {
|
|
2191
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Generate migration file content
|
|
2195
|
+
*/ generateMigrationContent(name) {
|
|
2196
|
+
return /*ts*/ `import type { SqlQuerier } from '@uql/migrate';
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Migration: ${name}
|
|
2200
|
+
* Created: ${new Date().toISOString()}
|
|
2201
|
+
*/
|
|
2202
|
+
export default {
|
|
2203
|
+
async up(querier: SqlQuerier): Promise<void> {
|
|
2204
|
+
// Add your migration logic here
|
|
2205
|
+
// Example:
|
|
2206
|
+
// await querier.run(\`
|
|
2207
|
+
// CREATE TABLE "users" (
|
|
2208
|
+
// "id" SERIAL PRIMARY KEY,
|
|
2209
|
+
// "name" VARCHAR(255) NOT NULL,
|
|
2210
|
+
// "email" VARCHAR(255) UNIQUE NOT NULL,
|
|
2211
|
+
// "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
2212
|
+
// )
|
|
2213
|
+
// \`);
|
|
2214
|
+
},
|
|
2215
|
+
|
|
2216
|
+
async down(querier: SqlQuerier): Promise<void> {
|
|
2217
|
+
// Add your rollback logic here
|
|
2218
|
+
// Example:
|
|
2219
|
+
// await querier.run(\`DROP TABLE IF EXISTS "users"\`);
|
|
2220
|
+
},
|
|
2221
|
+
};
|
|
2222
|
+
`;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Generate migration file content with SQL statements
|
|
2226
|
+
*/ generateMigrationContentWithStatements(name, upStatements, downStatements) {
|
|
2227
|
+
const upSql = upStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
|
|
2228
|
+
const downSql = downStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
|
|
2229
|
+
return /*ts*/ `import type { SqlQuerier } from '@uql/migrate';
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Migration: ${name}
|
|
2233
|
+
* Created: ${new Date().toISOString()}
|
|
2234
|
+
* Generated from entity definitions
|
|
2235
|
+
*/
|
|
2236
|
+
export default {
|
|
2237
|
+
async up(querier: SqlQuerier): Promise<void> {
|
|
2238
|
+
${upSql}
|
|
2239
|
+
},
|
|
2240
|
+
|
|
2241
|
+
async down(querier: SqlQuerier): Promise<void> {
|
|
2242
|
+
${downSql}
|
|
2243
|
+
},
|
|
2244
|
+
};
|
|
2245
|
+
`;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Helper function to define a migration with proper typing
|
|
2250
|
+
*/ function defineMigration(migration) {
|
|
2251
|
+
return migration;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* Stores migration state in a JSON file (useful for development/testing)
|
|
2256
|
+
*/ class JsonMigrationStorage {
|
|
2257
|
+
constructor(filePath = './migrations/.uql-migrations.json'){
|
|
2258
|
+
this.cache = null;
|
|
2259
|
+
this.filePath = filePath;
|
|
2260
|
+
}
|
|
2261
|
+
async ensureStorage() {
|
|
2262
|
+
try {
|
|
2263
|
+
await this.load();
|
|
2264
|
+
} catch {
|
|
2265
|
+
// File doesn't exist, create it
|
|
2266
|
+
await this.save([]);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async executed() {
|
|
2270
|
+
await this.ensureStorage();
|
|
2271
|
+
return this.cache ?? [];
|
|
2272
|
+
}
|
|
2273
|
+
async logWithQuerier(_querier, migrationName) {
|
|
2274
|
+
const executed = await this.executed();
|
|
2275
|
+
if (!executed.includes(migrationName)) {
|
|
2276
|
+
executed.push(migrationName);
|
|
2277
|
+
executed.sort();
|
|
2278
|
+
await this.save(executed);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
async unlogWithQuerier(_querier, migrationName) {
|
|
2282
|
+
const executed = await this.executed();
|
|
2283
|
+
const index = executed.indexOf(migrationName);
|
|
2284
|
+
if (index !== -1) {
|
|
2285
|
+
executed.splice(index, 1);
|
|
2286
|
+
await this.save(executed);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
async load() {
|
|
2290
|
+
const content = await readFile(this.filePath, 'utf-8');
|
|
2291
|
+
this.cache = JSON.parse(content);
|
|
2292
|
+
}
|
|
2293
|
+
async save(migrations) {
|
|
2294
|
+
await mkdir(dirname(this.filePath), {
|
|
2295
|
+
recursive: true
|
|
2296
|
+
});
|
|
2297
|
+
await writeFile(this.filePath, JSON.stringify(migrations, null, 2), 'utf-8');
|
|
2298
|
+
this.cache = migrations;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
export { AbstractSchemaGenerator, DatabaseMigrationStorage, JsonMigrationStorage, MysqlSchemaGenerator as MariadbSchemaGenerator, MariadbSchemaIntrospector, Migrator, MysqlSchemaGenerator, MysqlSchemaIntrospector, PostgresSchemaGenerator, PostgresSchemaIntrospector, SqliteSchemaGenerator, SqliteSchemaIntrospector, defineMigration };
|
|
2303
|
+
//# sourceMappingURL=uql-browser.min.js.map
|