cynx-lib 1.3.41 → 1.3.43
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/index.cjs +2 -0
- package/package.json +33 -9
- package/ui/index.cjs +182 -0
- package/utils/index.cjs +80 -0
- package/utils/utils/db.cjs +779 -0
- package/utils/utils/duration.cjs +119 -0
- package/utils/utils/oidc.cjs +164 -0
- package/utils/utils/realtime.cjs +881 -0
- package/utils/utils/session.cjs +61 -0
- package/utils/utils/storage.cjs +344 -0
- package/utils/utils/storage_manager.cjs +1177 -0
- package/utils/utils/tz.cjs +542 -0
- package/utils/utils/ui.cjs +105 -0
- package/utils/utils/uid.cjs +187 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var db_exports = {};
|
|
29
|
+
__export(db_exports, {
|
|
30
|
+
default: () => db_default,
|
|
31
|
+
dropAllTables: () => dropAllTables,
|
|
32
|
+
dropDb: () => dropDb,
|
|
33
|
+
drop_db: () => drop_db,
|
|
34
|
+
initDb: () => initDb,
|
|
35
|
+
init_db: () => init_db,
|
|
36
|
+
parseSchema: () => parseSchema,
|
|
37
|
+
parseSchemaAlterColumns: () => parseSchemaAlterColumns,
|
|
38
|
+
parseSchemaDoBlocks: () => parseSchemaDoBlocks,
|
|
39
|
+
parseSchemaDropConstraints: () => parseSchemaDropConstraints,
|
|
40
|
+
parseSchemaDropIndexes: () => parseSchemaDropIndexes,
|
|
41
|
+
parseSchemaIndexes: () => parseSchemaIndexes,
|
|
42
|
+
parseSchemaTables: () => parseSchemaTables,
|
|
43
|
+
resetDb: () => resetDb,
|
|
44
|
+
resetTable: () => resetTable,
|
|
45
|
+
reset_db: () => reset_db,
|
|
46
|
+
reset_table: () => reset_table,
|
|
47
|
+
runSafeDdl: () => runSafeDdl,
|
|
48
|
+
syncDatabaseSchema: () => syncDatabaseSchema,
|
|
49
|
+
syncDb: () => syncDb,
|
|
50
|
+
sync_db: () => sync_db,
|
|
51
|
+
truncateTable: () => truncateTable,
|
|
52
|
+
truncate_table: () => truncate_table
|
|
53
|
+
});
|
|
54
|
+
module.exports = __toCommonJS(db_exports);
|
|
55
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
56
|
+
var import_path = __toESM(require("path"), 1);
|
|
57
|
+
async function runSafeDdl(queryFn, sql, label = "DDL", { maxRetries = 3, retryDelayMs = 300, logger = console } = {}) {
|
|
58
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
await queryFn(sql);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const msg = err && err.message ? err.message : String(err);
|
|
64
|
+
const isLockTimeout = msg.includes("lock timeout") || msg.includes("deadlock") || msg.includes("canceling statement due to lock timeout");
|
|
65
|
+
if (isLockTimeout && attempt < maxRetries) {
|
|
66
|
+
if (logger && logger.warn) {
|
|
67
|
+
logger.warn(`[DB Sync] Lock timeout on ${label} (attempt ${attempt}/${maxRetries}), retrying in ${retryDelayMs * attempt}ms...`);
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (logger && logger.warn) {
|
|
73
|
+
logger.warn(`[DB Sync] Notice on ${label}: ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function parseSchemaTables(schemaSql) {
|
|
81
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
82
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z0-9_]+)\s*\(([\s\S]*?)\);/gi;
|
|
83
|
+
let match;
|
|
84
|
+
const tables = [];
|
|
85
|
+
while ((match = tableRegex.exec(schemaSql)) !== null) {
|
|
86
|
+
const tableName = match[1].toLowerCase();
|
|
87
|
+
const body = match[2];
|
|
88
|
+
const columns = [];
|
|
89
|
+
const lines = body.split("\n");
|
|
90
|
+
for (const rawLine of lines) {
|
|
91
|
+
const line = rawLine.trim().replace(/,$/, "");
|
|
92
|
+
if (!line || line.startsWith("--") || line.startsWith("/*")) continue;
|
|
93
|
+
if (/^(PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CONSTRAINT|CHECK)\b/i.test(line)) continue;
|
|
94
|
+
const colMatch = line.match(/^([a-zA-Z0-9_]+)\s+([\s\S]+)$/i);
|
|
95
|
+
if (colMatch) {
|
|
96
|
+
const colName = colMatch[1].toLowerCase();
|
|
97
|
+
let colDef = colMatch[2].trim();
|
|
98
|
+
colDef = colDef.replace(/\bPRIMARY\s+KEY\b/gi, "").trim();
|
|
99
|
+
columns.push({ name: colName, def: colDef, raw: line });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
tables.push({
|
|
103
|
+
name: tableName,
|
|
104
|
+
columns,
|
|
105
|
+
fullCreateSql: match[0]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return tables;
|
|
109
|
+
}
|
|
110
|
+
function parseSchemaAlterColumns(schemaSql) {
|
|
111
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
112
|
+
const alterAddRegex = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([a-zA-Z0-9_]+)\s+ADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z0-9_]+)\s+([^;]+);/gi;
|
|
113
|
+
let match;
|
|
114
|
+
const alterAdds = [];
|
|
115
|
+
while ((match = alterAddRegex.exec(schemaSql)) !== null) {
|
|
116
|
+
alterAdds.push({
|
|
117
|
+
table: match[1].toLowerCase(),
|
|
118
|
+
column: match[2].toLowerCase(),
|
|
119
|
+
def: match[3].trim(),
|
|
120
|
+
raw: match[0]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return alterAdds;
|
|
124
|
+
}
|
|
125
|
+
function parseSchemaDropConstraints(schemaSql) {
|
|
126
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
127
|
+
const dropConstRegex = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([a-zA-Z0-9_]+)\s+DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?([a-zA-Z0-9_]+);/gi;
|
|
128
|
+
let match;
|
|
129
|
+
const dropConstraints = [];
|
|
130
|
+
while ((match = dropConstRegex.exec(schemaSql)) !== null) {
|
|
131
|
+
dropConstraints.push({
|
|
132
|
+
table: match[1].toLowerCase(),
|
|
133
|
+
constraint: match[2].toLowerCase(),
|
|
134
|
+
raw: match[0]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return dropConstraints;
|
|
138
|
+
}
|
|
139
|
+
function parseSchemaDropIndexes(schemaSql) {
|
|
140
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
141
|
+
const dropIndexRegex = /DROP\s+INDEX\s+(?:IF\s+EXISTS\s+)?([a-zA-Z0-9_]+);/gi;
|
|
142
|
+
let match;
|
|
143
|
+
const dropIndexes = [];
|
|
144
|
+
while ((match = dropIndexRegex.exec(schemaSql)) !== null) {
|
|
145
|
+
dropIndexes.push({
|
|
146
|
+
name: match[1].toLowerCase(),
|
|
147
|
+
raw: match[0]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return dropIndexes;
|
|
151
|
+
}
|
|
152
|
+
function parseSchemaIndexes(schemaSql) {
|
|
153
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
154
|
+
const indexRegex = /CREATE\s+(UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z0-9_]+)\s+ON\s+([a-zA-Z0-9_]+)\s*([\s\S]*?);/gi;
|
|
155
|
+
let match;
|
|
156
|
+
const indexes = [];
|
|
157
|
+
while ((match = indexRegex.exec(schemaSql)) !== null) {
|
|
158
|
+
indexes.push({
|
|
159
|
+
unique: Boolean(match[1]),
|
|
160
|
+
name: match[2].toLowerCase(),
|
|
161
|
+
table: match[3].toLowerCase(),
|
|
162
|
+
rest: match[4].trim(),
|
|
163
|
+
raw: match[0]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return indexes;
|
|
167
|
+
}
|
|
168
|
+
function parseSchemaDoBlocks(schemaSql) {
|
|
169
|
+
if (!schemaSql || typeof schemaSql !== "string") return [];
|
|
170
|
+
const doRegex = /DO\s+\$\$\s*BEGIN([\s\S]*?)END\s+\$\$;/gi;
|
|
171
|
+
let match;
|
|
172
|
+
const doBlocks = [];
|
|
173
|
+
while ((match = doRegex.exec(schemaSql)) !== null) {
|
|
174
|
+
doBlocks.push(match[0]);
|
|
175
|
+
}
|
|
176
|
+
return doBlocks;
|
|
177
|
+
}
|
|
178
|
+
function parseSchema(schemaSql) {
|
|
179
|
+
return {
|
|
180
|
+
tables: parseSchemaTables(schemaSql),
|
|
181
|
+
alterAdds: parseSchemaAlterColumns(schemaSql),
|
|
182
|
+
dropConstraints: parseSchemaDropConstraints(schemaSql),
|
|
183
|
+
dropIndexes: parseSchemaDropIndexes(schemaSql),
|
|
184
|
+
indexes: parseSchemaIndexes(schemaSql),
|
|
185
|
+
doBlocks: parseSchemaDoBlocks(schemaSql)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function sync_db(options = {}) {
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
const {
|
|
191
|
+
pool,
|
|
192
|
+
query: customQuery,
|
|
193
|
+
schemaSql: directSql,
|
|
194
|
+
schemaPath,
|
|
195
|
+
tableSchema = "public",
|
|
196
|
+
freshCheckTable,
|
|
197
|
+
logger: rawLogger = console,
|
|
198
|
+
dryRun = false,
|
|
199
|
+
maxRetries = 3,
|
|
200
|
+
retryDelayMs = 300,
|
|
201
|
+
throwOnError = false
|
|
202
|
+
} = options;
|
|
203
|
+
const logger = rawLogger === false ? null : rawLogger || console;
|
|
204
|
+
const log = (msg) => {
|
|
205
|
+
if (logger && logger.log) logger.log(`[DB Sync] ${msg}`);
|
|
206
|
+
};
|
|
207
|
+
const warn = (msg) => {
|
|
208
|
+
if (logger && logger.warn) logger.warn(`[DB Sync] ${msg}`);
|
|
209
|
+
};
|
|
210
|
+
const error = (msg, err) => {
|
|
211
|
+
if (logger && logger.error) logger.error(`[DB Sync] ${msg}`, err || "");
|
|
212
|
+
};
|
|
213
|
+
let queryFn = customQuery;
|
|
214
|
+
if (!queryFn && pool) {
|
|
215
|
+
queryFn = (text, params) => pool.query(text, params);
|
|
216
|
+
}
|
|
217
|
+
if (typeof queryFn !== "function") {
|
|
218
|
+
const err = new Error("[cynx/db] No database query function or pool provided to sync_db()");
|
|
219
|
+
error(err.message);
|
|
220
|
+
if (throwOnError) throw err;
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
isFreshDb: false,
|
|
224
|
+
createdTables: [],
|
|
225
|
+
addedColumns: [],
|
|
226
|
+
createdIndexes: [],
|
|
227
|
+
droppedConstraints: [],
|
|
228
|
+
droppedIndexes: [],
|
|
229
|
+
executedDoBlocks: 0,
|
|
230
|
+
changesCount: 0,
|
|
231
|
+
durationMs: 0,
|
|
232
|
+
summary: "Failed: No query executor",
|
|
233
|
+
error: err
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
let schemaSql = directSql;
|
|
237
|
+
if (!schemaSql && schemaPath) {
|
|
238
|
+
const resolvedPath = import_path.default.resolve(schemaPath);
|
|
239
|
+
if (!import_fs.default.existsSync(resolvedPath)) {
|
|
240
|
+
const err = new Error(`[cynx/db] schema.sql not found at: ${resolvedPath}`);
|
|
241
|
+
warn(err.message);
|
|
242
|
+
if (throwOnError) throw err;
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
isFreshDb: false,
|
|
246
|
+
createdTables: [],
|
|
247
|
+
addedColumns: [],
|
|
248
|
+
createdIndexes: [],
|
|
249
|
+
droppedConstraints: [],
|
|
250
|
+
droppedIndexes: [],
|
|
251
|
+
executedDoBlocks: 0,
|
|
252
|
+
changesCount: 0,
|
|
253
|
+
durationMs: 0,
|
|
254
|
+
summary: "Failed: schema.sql file not found",
|
|
255
|
+
error: err
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
schemaSql = import_fs.default.readFileSync(resolvedPath, "utf-8");
|
|
259
|
+
}
|
|
260
|
+
if (!schemaSql || typeof schemaSql !== "string" || !schemaSql.trim()) {
|
|
261
|
+
const err = new Error("[cynx/db] No schema SQL provided to sync_db()");
|
|
262
|
+
warn(err.message);
|
|
263
|
+
if (throwOnError) throw err;
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
isFreshDb: false,
|
|
267
|
+
createdTables: [],
|
|
268
|
+
addedColumns: [],
|
|
269
|
+
createdIndexes: [],
|
|
270
|
+
droppedConstraints: [],
|
|
271
|
+
droppedIndexes: [],
|
|
272
|
+
executedDoBlocks: 0,
|
|
273
|
+
changesCount: 0,
|
|
274
|
+
durationMs: 0,
|
|
275
|
+
summary: "Failed: Empty schema SQL",
|
|
276
|
+
error: err
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
let isFreshDb = false;
|
|
281
|
+
if (freshCheckTable) {
|
|
282
|
+
const tableCheck = await queryFn(
|
|
283
|
+
"SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1",
|
|
284
|
+
[tableSchema, String(freshCheckTable).toLowerCase()]
|
|
285
|
+
);
|
|
286
|
+
isFreshDb = tableCheck.rowCount === 0;
|
|
287
|
+
} else {
|
|
288
|
+
const countCheck = await queryFn(
|
|
289
|
+
"SELECT count(*)::int as count FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'",
|
|
290
|
+
[tableSchema]
|
|
291
|
+
);
|
|
292
|
+
const tableCount = countCheck.rows && countCheck.rows[0] ? Number(countCheck.rows[0].count) : 0;
|
|
293
|
+
isFreshDb = tableCount === 0;
|
|
294
|
+
}
|
|
295
|
+
if (isFreshDb) {
|
|
296
|
+
log("Fresh database detected. Applying full schema.sql in one pass...");
|
|
297
|
+
if (!dryRun) {
|
|
298
|
+
await queryFn(schemaSql);
|
|
299
|
+
}
|
|
300
|
+
const durationMs2 = Date.now() - startTime;
|
|
301
|
+
log(`Fresh database schema initialized successfully (${durationMs2}ms)`);
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
isFreshDb: true,
|
|
305
|
+
createdTables: ["* (full schema applied)"],
|
|
306
|
+
addedColumns: [],
|
|
307
|
+
createdIndexes: [],
|
|
308
|
+
droppedConstraints: [],
|
|
309
|
+
droppedIndexes: [],
|
|
310
|
+
executedDoBlocks: 0,
|
|
311
|
+
changesCount: 1,
|
|
312
|
+
durationMs: durationMs2,
|
|
313
|
+
summary: `Fresh database initialized in ${durationMs2}ms`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
log("Analyzing schema.sql and comparing with database catalogs (Zero-Lock)...");
|
|
317
|
+
const [tablesRes, colsRes, idxRes, conRes] = await Promise.all([
|
|
318
|
+
queryFn("SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'", [tableSchema]),
|
|
319
|
+
queryFn("SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = $1", [tableSchema]),
|
|
320
|
+
queryFn("SELECT indexname FROM pg_indexes WHERE schemaname = $1", [tableSchema]),
|
|
321
|
+
queryFn("SELECT conname FROM pg_constraint")
|
|
322
|
+
]);
|
|
323
|
+
const existingTables = new Set((tablesRes.rows || []).map((r) => String(r.table_name).toLowerCase()));
|
|
324
|
+
const existingColumns = new Set((colsRes.rows || []).map((r) => `${String(r.table_name).toLowerCase()}.${String(r.column_name).toLowerCase()}`));
|
|
325
|
+
const existingIndexes = new Set((idxRes.rows || []).map((r) => String(r.indexname).toLowerCase()));
|
|
326
|
+
const existingConstraints = new Set((conRes.rows || []).map((r) => String(r.conname).toLowerCase()));
|
|
327
|
+
const createdTables = [];
|
|
328
|
+
const addedColumns = [];
|
|
329
|
+
const createdIndexes = [];
|
|
330
|
+
const droppedConstraints = [];
|
|
331
|
+
const droppedIndexes = [];
|
|
332
|
+
let executedDoBlocks = 0;
|
|
333
|
+
const parsed = parseSchema(schemaSql);
|
|
334
|
+
const safeDdlOpts = { maxRetries, retryDelayMs, logger };
|
|
335
|
+
for (const doSql of parsed.doBlocks) {
|
|
336
|
+
if (!dryRun) {
|
|
337
|
+
await runSafeDdl(queryFn, doSql, "schema do-block", safeDdlOpts);
|
|
338
|
+
}
|
|
339
|
+
executedDoBlocks++;
|
|
340
|
+
}
|
|
341
|
+
for (const table of parsed.tables) {
|
|
342
|
+
if (!existingTables.has(table.name)) {
|
|
343
|
+
if (!dryRun) {
|
|
344
|
+
const ok = await runSafeDdl(queryFn, table.fullCreateSql, `create table ${table.name}`, safeDdlOpts);
|
|
345
|
+
if (ok) {
|
|
346
|
+
existingTables.add(table.name);
|
|
347
|
+
createdTables.push(table.name);
|
|
348
|
+
for (const col of table.columns) {
|
|
349
|
+
existingColumns.add(`${table.name}.${col.name}`);
|
|
350
|
+
}
|
|
351
|
+
log(`Created missing table: ${table.name}`);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
createdTables.push(table.name);
|
|
355
|
+
log(`[DryRun] Would create table: ${table.name}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const table of parsed.tables) {
|
|
360
|
+
if (existingTables.has(table.name)) {
|
|
361
|
+
for (const col of table.columns) {
|
|
362
|
+
const colKey = `${table.name}.${col.name}`;
|
|
363
|
+
if (!existingColumns.has(colKey)) {
|
|
364
|
+
const sql = `ALTER TABLE ${table.name} ADD COLUMN IF NOT EXISTS ${col.name} ${col.def};`;
|
|
365
|
+
if (!dryRun) {
|
|
366
|
+
const ok = await runSafeDdl(queryFn, sql, `add column ${colKey}`, safeDdlOpts);
|
|
367
|
+
if (ok) {
|
|
368
|
+
existingColumns.add(colKey);
|
|
369
|
+
addedColumns.push(colKey);
|
|
370
|
+
log(`Added missing column: ${colKey}`);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
addedColumns.push(colKey);
|
|
374
|
+
log(`[DryRun] Would add column: ${colKey}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const alter of parsed.alterAdds) {
|
|
381
|
+
const colKey = `${alter.table}.${alter.column}`;
|
|
382
|
+
if (existingTables.has(alter.table) && !existingColumns.has(colKey)) {
|
|
383
|
+
const sql = `ALTER TABLE ${alter.table} ADD COLUMN IF NOT EXISTS ${alter.column} ${alter.def};`;
|
|
384
|
+
if (!dryRun) {
|
|
385
|
+
const ok = await runSafeDdl(queryFn, sql, `alter add column ${colKey}`, safeDdlOpts);
|
|
386
|
+
if (ok) {
|
|
387
|
+
existingColumns.add(colKey);
|
|
388
|
+
addedColumns.push(colKey);
|
|
389
|
+
log(`Added missing column: ${colKey}`);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
addedColumns.push(colKey);
|
|
393
|
+
log(`[DryRun] Would add column: ${colKey}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const drop of parsed.dropConstraints) {
|
|
398
|
+
if (existingConstraints.has(drop.constraint)) {
|
|
399
|
+
if (!dryRun) {
|
|
400
|
+
const ok = await runSafeDdl(queryFn, drop.raw, `drop constraint ${drop.constraint}`, safeDdlOpts);
|
|
401
|
+
if (ok) {
|
|
402
|
+
existingConstraints.delete(drop.constraint);
|
|
403
|
+
droppedConstraints.push(`${drop.table}.${drop.constraint}`);
|
|
404
|
+
log(`Dropped obsolete constraint: ${drop.table}.${drop.constraint}`);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
droppedConstraints.push(`${drop.table}.${drop.constraint}`);
|
|
408
|
+
log(`[DryRun] Would drop constraint: ${drop.table}.${drop.constraint}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const drop of parsed.dropIndexes) {
|
|
413
|
+
if (existingIndexes.has(drop.name)) {
|
|
414
|
+
if (!dryRun) {
|
|
415
|
+
const ok = await runSafeDdl(queryFn, drop.raw, `drop index ${drop.name}`, safeDdlOpts);
|
|
416
|
+
if (ok) {
|
|
417
|
+
existingIndexes.delete(drop.name);
|
|
418
|
+
droppedIndexes.push(drop.name);
|
|
419
|
+
log(`Dropped obsolete index: ${drop.name}`);
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
droppedIndexes.push(drop.name);
|
|
423
|
+
log(`[DryRun] Would drop index: ${drop.name}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const idx of parsed.indexes) {
|
|
428
|
+
if (existingTables.has(idx.table) && !existingIndexes.has(idx.name)) {
|
|
429
|
+
const uniqueClause = idx.unique ? "UNIQUE " : "";
|
|
430
|
+
const sql = `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${idx.name} ON ${idx.table} ${idx.rest};`;
|
|
431
|
+
if (!dryRun) {
|
|
432
|
+
const ok = await runSafeDdl(queryFn, sql, `create index ${idx.name}`, safeDdlOpts);
|
|
433
|
+
if (ok) {
|
|
434
|
+
existingIndexes.add(idx.name);
|
|
435
|
+
createdIndexes.push(idx.name);
|
|
436
|
+
log(`Created index: ${idx.name}`);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
createdIndexes.push(idx.name);
|
|
440
|
+
log(`[DryRun] Would create index: ${idx.name}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const durationMs = Date.now() - startTime;
|
|
445
|
+
const summaryParts = [];
|
|
446
|
+
if (createdTables.length > 0) summaryParts.push(`${createdTables.length} tables created`);
|
|
447
|
+
if (addedColumns.length > 0) summaryParts.push(`${addedColumns.length} columns added`);
|
|
448
|
+
if (createdIndexes.length > 0) summaryParts.push(`${createdIndexes.length} indexes created`);
|
|
449
|
+
if (droppedConstraints.length > 0) summaryParts.push(`${droppedConstraints.length} constraints dropped`);
|
|
450
|
+
if (droppedIndexes.length > 0) summaryParts.push(`${droppedIndexes.length} indexes dropped`);
|
|
451
|
+
if (executedDoBlocks > 0) summaryParts.push(`${executedDoBlocks} migration blocks run`);
|
|
452
|
+
const changesCount = createdTables.length + addedColumns.length + createdIndexes.length + droppedConstraints.length + droppedIndexes.length;
|
|
453
|
+
const summary = summaryParts.length > 0 ? `Database synced in ${durationMs}ms: ${summaryParts.join(", ")}` : `Database schema is up to date (0 changes needed, ${durationMs}ms)`;
|
|
454
|
+
log(summary);
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
isFreshDb: false,
|
|
458
|
+
createdTables,
|
|
459
|
+
addedColumns,
|
|
460
|
+
createdIndexes,
|
|
461
|
+
droppedConstraints,
|
|
462
|
+
droppedIndexes,
|
|
463
|
+
executedDoBlocks,
|
|
464
|
+
changesCount,
|
|
465
|
+
durationMs,
|
|
466
|
+
summary
|
|
467
|
+
};
|
|
468
|
+
} catch (err) {
|
|
469
|
+
error("Database sync failed:", err.message);
|
|
470
|
+
if (throwOnError) throw err;
|
|
471
|
+
return {
|
|
472
|
+
success: false,
|
|
473
|
+
isFreshDb: false,
|
|
474
|
+
createdTables: [],
|
|
475
|
+
addedColumns: [],
|
|
476
|
+
createdIndexes: [],
|
|
477
|
+
droppedConstraints: [],
|
|
478
|
+
droppedIndexes: [],
|
|
479
|
+
executedDoBlocks: 0,
|
|
480
|
+
changesCount: 0,
|
|
481
|
+
durationMs: Date.now() - startTime,
|
|
482
|
+
summary: `Failed: ${err.message}`,
|
|
483
|
+
error: err
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function drop_db(options = {}) {
|
|
488
|
+
const startTime = Date.now();
|
|
489
|
+
const {
|
|
490
|
+
pool,
|
|
491
|
+
query: customQuery,
|
|
492
|
+
tableSchema = "public",
|
|
493
|
+
logger: rawLogger = console,
|
|
494
|
+
dryRun = false
|
|
495
|
+
} = options;
|
|
496
|
+
const logger = rawLogger === false ? null : rawLogger || console;
|
|
497
|
+
const log = (msg) => {
|
|
498
|
+
if (logger && logger.log) logger.log(`[DB Drop] ${msg}`);
|
|
499
|
+
};
|
|
500
|
+
const error = (msg, err) => {
|
|
501
|
+
if (logger && logger.error) logger.error(`[DB Drop] ${msg}`, err || "");
|
|
502
|
+
};
|
|
503
|
+
let queryFn = customQuery;
|
|
504
|
+
if (!queryFn && pool) {
|
|
505
|
+
queryFn = (text, params) => pool.query(text, params);
|
|
506
|
+
}
|
|
507
|
+
if (typeof queryFn !== "function") {
|
|
508
|
+
const err = new Error("[cynx/db] No database query function or pool provided to drop_db()");
|
|
509
|
+
error(err.message);
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
droppedTables: [],
|
|
513
|
+
durationMs: 0,
|
|
514
|
+
summary: "Failed: No query executor",
|
|
515
|
+
error: err
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const res = await queryFn(
|
|
520
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'",
|
|
521
|
+
[tableSchema]
|
|
522
|
+
);
|
|
523
|
+
const tables = (res.rows || []).map((r) => r.table_name);
|
|
524
|
+
if (tables.length === 0) {
|
|
525
|
+
log(`No tables found in schema '${tableSchema}'. Nothing to drop.`);
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
droppedTables: [],
|
|
529
|
+
durationMs: Date.now() - startTime,
|
|
530
|
+
summary: `0 tables dropped (schema '${tableSchema}' was already empty)`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (dryRun) {
|
|
534
|
+
log(`[DryRun] Would drop ${tables.length} tables: ${tables.join(", ")}`);
|
|
535
|
+
return {
|
|
536
|
+
success: true,
|
|
537
|
+
droppedTables: tables,
|
|
538
|
+
durationMs: Date.now() - startTime,
|
|
539
|
+
summary: `[DryRun] ${tables.length} tables listed for drop`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
log(`Dropping ${tables.length} tables from schema '${tableSchema}' with CASCADE...`);
|
|
543
|
+
const dropSql = `
|
|
544
|
+
DO $$ DECLARE
|
|
545
|
+
r RECORD;
|
|
546
|
+
BEGIN
|
|
547
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${tableSchema.replace(/'/g, "''")}') LOOP
|
|
548
|
+
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
549
|
+
END LOOP;
|
|
550
|
+
END $$;
|
|
551
|
+
`;
|
|
552
|
+
await queryFn(dropSql);
|
|
553
|
+
const durationMs = Date.now() - startTime;
|
|
554
|
+
const summary = `Successfully dropped ${tables.length} tables in ${durationMs}ms`;
|
|
555
|
+
log(summary);
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
droppedTables: tables,
|
|
559
|
+
durationMs,
|
|
560
|
+
summary
|
|
561
|
+
};
|
|
562
|
+
} catch (err) {
|
|
563
|
+
error("Failed to drop tables:", err.message);
|
|
564
|
+
return {
|
|
565
|
+
success: false,
|
|
566
|
+
droppedTables: [],
|
|
567
|
+
durationMs: Date.now() - startTime,
|
|
568
|
+
summary: `Failed: ${err.message}`,
|
|
569
|
+
error: err
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function reset_table(tableOrOptions, options = {}) {
|
|
574
|
+
const startTime = Date.now();
|
|
575
|
+
const opts = typeof tableOrOptions === "object" && tableOrOptions !== null ? { ...tableOrOptions, ...options } : { ...options, table: tableOrOptions };
|
|
576
|
+
const {
|
|
577
|
+
table,
|
|
578
|
+
pool,
|
|
579
|
+
query: customQuery,
|
|
580
|
+
where,
|
|
581
|
+
cascade = true,
|
|
582
|
+
restartIdentity = true,
|
|
583
|
+
recreate = false,
|
|
584
|
+
schemaSql,
|
|
585
|
+
schemaPath,
|
|
586
|
+
logger: rawLogger = console
|
|
587
|
+
} = opts;
|
|
588
|
+
const logger = rawLogger === false ? null : rawLogger || console;
|
|
589
|
+
const log = (msg) => {
|
|
590
|
+
if (logger && logger.log) logger.log(`[DB Reset Table] ${msg}`);
|
|
591
|
+
};
|
|
592
|
+
const error = (msg, err) => {
|
|
593
|
+
if (logger && logger.error) logger.error(`[DB Reset Table] ${msg}`, err || "");
|
|
594
|
+
};
|
|
595
|
+
if (!table || typeof table !== "string") {
|
|
596
|
+
const err = new Error("[cynx/db] Table name is required for reset_table()");
|
|
597
|
+
error(err.message);
|
|
598
|
+
return {
|
|
599
|
+
success: false,
|
|
600
|
+
table: "",
|
|
601
|
+
mode: "truncate",
|
|
602
|
+
durationMs: 0,
|
|
603
|
+
summary: "Failed: Missing table name",
|
|
604
|
+
error: err
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const cleanTable = table.trim().toLowerCase();
|
|
608
|
+
let queryFn = customQuery;
|
|
609
|
+
if (!queryFn && pool) {
|
|
610
|
+
queryFn = (text, params) => pool.query(text, params);
|
|
611
|
+
}
|
|
612
|
+
if (typeof queryFn !== "function") {
|
|
613
|
+
const err = new Error("[cynx/db] No database query function or pool provided to reset_table()");
|
|
614
|
+
error(err.message);
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
table: cleanTable,
|
|
618
|
+
mode: "truncate",
|
|
619
|
+
durationMs: 0,
|
|
620
|
+
summary: "Failed: No query executor",
|
|
621
|
+
error: err
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
if (where) {
|
|
626
|
+
let whereClause = "";
|
|
627
|
+
let params = [];
|
|
628
|
+
if (typeof where === "string") {
|
|
629
|
+
whereClause = where.trim().replace(/^WHERE\s+/i, "");
|
|
630
|
+
} else if (Array.isArray(where) && where.length > 0) {
|
|
631
|
+
whereClause = where[0].replace(/^WHERE\s+/i, "");
|
|
632
|
+
params = Array.isArray(where[1]) ? where[1] : [];
|
|
633
|
+
} else if (typeof where === "object") {
|
|
634
|
+
const conditions = [];
|
|
635
|
+
let pIndex = 1;
|
|
636
|
+
for (const [key, val] of Object.entries(where)) {
|
|
637
|
+
if (val === null) {
|
|
638
|
+
conditions.push(`${key} IS NULL`);
|
|
639
|
+
} else {
|
|
640
|
+
conditions.push(`${key} = $${pIndex++}`);
|
|
641
|
+
params.push(val);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
whereClause = conditions.join(" AND ");
|
|
645
|
+
}
|
|
646
|
+
if (!whereClause) {
|
|
647
|
+
throw new Error("[cynx/db] Invalid WHERE condition provided for row reset");
|
|
648
|
+
}
|
|
649
|
+
const sql2 = `DELETE FROM ${cleanTable} WHERE ${whereClause};`;
|
|
650
|
+
const res = await queryFn(sql2, params);
|
|
651
|
+
const rowCount = res && typeof res.rowCount === "number" ? res.rowCount : 0;
|
|
652
|
+
const durationMs2 = Date.now() - startTime;
|
|
653
|
+
const summary2 = `Deleted ${rowCount} rows from table '${cleanTable}' (${durationMs2}ms)`;
|
|
654
|
+
log(summary2);
|
|
655
|
+
return {
|
|
656
|
+
success: true,
|
|
657
|
+
table: cleanTable,
|
|
658
|
+
mode: "delete",
|
|
659
|
+
rowCount,
|
|
660
|
+
durationMs: durationMs2,
|
|
661
|
+
summary: summary2
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (recreate) {
|
|
665
|
+
log(`Dropping table '${cleanTable}' with CASCADE for full recreation...`);
|
|
666
|
+
await queryFn(`DROP TABLE IF EXISTS ${cleanTable} CASCADE;`);
|
|
667
|
+
log(`Recreating table '${cleanTable}' via sync_db...`);
|
|
668
|
+
const syncResult = await sync_db({
|
|
669
|
+
pool,
|
|
670
|
+
query: queryFn,
|
|
671
|
+
schemaSql,
|
|
672
|
+
schemaPath,
|
|
673
|
+
logger
|
|
674
|
+
});
|
|
675
|
+
const durationMs2 = Date.now() - startTime;
|
|
676
|
+
const summary2 = `Recreated table '${cleanTable}' from schema (${durationMs2}ms)`;
|
|
677
|
+
log(summary2);
|
|
678
|
+
return {
|
|
679
|
+
success: syncResult.success,
|
|
680
|
+
table: cleanTable,
|
|
681
|
+
mode: "recreate",
|
|
682
|
+
durationMs: durationMs2,
|
|
683
|
+
summary: summary2
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const restartClause = restartIdentity ? " RESTART IDENTITY" : "";
|
|
687
|
+
const cascadeClause = cascade ? " CASCADE" : "";
|
|
688
|
+
const sql = `TRUNCATE TABLE ${cleanTable}${restartClause}${cascadeClause};`;
|
|
689
|
+
await queryFn(sql);
|
|
690
|
+
const durationMs = Date.now() - startTime;
|
|
691
|
+
const summary = `Truncated table '${cleanTable}'${restartClause}${cascadeClause} in ${durationMs}ms`;
|
|
692
|
+
log(summary);
|
|
693
|
+
return {
|
|
694
|
+
success: true,
|
|
695
|
+
table: cleanTable,
|
|
696
|
+
mode: "truncate",
|
|
697
|
+
durationMs,
|
|
698
|
+
summary
|
|
699
|
+
};
|
|
700
|
+
} catch (err) {
|
|
701
|
+
error(`Failed to reset table '${cleanTable}':`, err.message);
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
table: cleanTable,
|
|
705
|
+
mode: where ? "delete" : recreate ? "recreate" : "truncate",
|
|
706
|
+
durationMs: Date.now() - startTime,
|
|
707
|
+
summary: `Failed: ${err.message}`,
|
|
708
|
+
error: err
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function reset_db(options = {}) {
|
|
713
|
+
const dropResult = await drop_db(options);
|
|
714
|
+
const syncResult = await sync_db(options);
|
|
715
|
+
return {
|
|
716
|
+
success: dropResult.success && syncResult.success,
|
|
717
|
+
dropResult,
|
|
718
|
+
syncResult
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const init_db = sync_db;
|
|
722
|
+
const initDb = sync_db;
|
|
723
|
+
const dropDb = drop_db;
|
|
724
|
+
const dropAllTables = drop_db;
|
|
725
|
+
const resetDb = reset_db;
|
|
726
|
+
const resetTable = reset_table;
|
|
727
|
+
const truncate_table = reset_table;
|
|
728
|
+
const truncateTable = reset_table;
|
|
729
|
+
const syncDb = sync_db;
|
|
730
|
+
const syncDatabaseSchema = sync_db;
|
|
731
|
+
var db_default = {
|
|
732
|
+
sync_db,
|
|
733
|
+
syncDb,
|
|
734
|
+
syncDatabaseSchema,
|
|
735
|
+
init_db,
|
|
736
|
+
initDb,
|
|
737
|
+
drop_db,
|
|
738
|
+
dropDb,
|
|
739
|
+
dropAllTables,
|
|
740
|
+
reset_db,
|
|
741
|
+
resetDb,
|
|
742
|
+
reset_table,
|
|
743
|
+
resetTable,
|
|
744
|
+
truncate_table,
|
|
745
|
+
truncateTable,
|
|
746
|
+
runSafeDdl,
|
|
747
|
+
parseSchema,
|
|
748
|
+
parseSchemaTables,
|
|
749
|
+
parseSchemaAlterColumns,
|
|
750
|
+
parseSchemaDropConstraints,
|
|
751
|
+
parseSchemaDropIndexes,
|
|
752
|
+
parseSchemaIndexes,
|
|
753
|
+
parseSchemaDoBlocks
|
|
754
|
+
};
|
|
755
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
756
|
+
0 && (module.exports = {
|
|
757
|
+
dropAllTables,
|
|
758
|
+
dropDb,
|
|
759
|
+
drop_db,
|
|
760
|
+
initDb,
|
|
761
|
+
init_db,
|
|
762
|
+
parseSchema,
|
|
763
|
+
parseSchemaAlterColumns,
|
|
764
|
+
parseSchemaDoBlocks,
|
|
765
|
+
parseSchemaDropConstraints,
|
|
766
|
+
parseSchemaDropIndexes,
|
|
767
|
+
parseSchemaIndexes,
|
|
768
|
+
parseSchemaTables,
|
|
769
|
+
resetDb,
|
|
770
|
+
resetTable,
|
|
771
|
+
reset_db,
|
|
772
|
+
reset_table,
|
|
773
|
+
runSafeDdl,
|
|
774
|
+
syncDatabaseSchema,
|
|
775
|
+
syncDb,
|
|
776
|
+
sync_db,
|
|
777
|
+
truncateTable,
|
|
778
|
+
truncate_table
|
|
779
|
+
});
|