@dyrected/db-mysql 2.15.0 → 2.17.0
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 +265 -39
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +266 -40
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,22 @@ __export(index_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
var import_core = require("@dyrected/core");
|
|
39
39
|
var import_promise = __toESM(require("mysql2/promise"), 1);
|
|
40
|
+
function parseMysqlDuplicateKey(message) {
|
|
41
|
+
const match = message.match(/Duplicate entry '(.*)' for key '(?:[^.]*\.)?(.*)'/);
|
|
42
|
+
if (!match) return {};
|
|
43
|
+
const [, value, keyName] = match;
|
|
44
|
+
let field = keyName;
|
|
45
|
+
if (keyName === "PRIMARY") {
|
|
46
|
+
field = "id";
|
|
47
|
+
} else if (keyName.startsWith("uniq_") || keyName.startsWith("idx_")) {
|
|
48
|
+
const parts = keyName.split("_");
|
|
49
|
+
field = parts[parts.length - 1];
|
|
50
|
+
}
|
|
51
|
+
return { field, value };
|
|
52
|
+
}
|
|
53
|
+
function isNumericOp(val) {
|
|
54
|
+
return val !== null && typeof val === "object" && !Array.isArray(val) && ("increment" in val || "decrement" in val);
|
|
55
|
+
}
|
|
40
56
|
var MYSQL_CLIENT_CACHE_KEY = "__dyrectedMysqlClientCache";
|
|
41
57
|
function getSharedMysqlClientCache() {
|
|
42
58
|
const globalScope = globalThis;
|
|
@@ -69,7 +85,9 @@ var MysqlAdapter = class {
|
|
|
69
85
|
inTransaction = false;
|
|
70
86
|
tableLocks = /* @__PURE__ */ new Map();
|
|
71
87
|
ensuredTables = /* @__PURE__ */ new Set();
|
|
88
|
+
failedIndexes = /* @__PURE__ */ new Set();
|
|
72
89
|
tableColumnsCache = /* @__PURE__ */ new Map();
|
|
90
|
+
collectionConfigs = /* @__PURE__ */ new Map();
|
|
73
91
|
constructor(config) {
|
|
74
92
|
this.config = config;
|
|
75
93
|
}
|
|
@@ -204,7 +222,7 @@ FIX INSTRUCTIONS:
|
|
|
204
222
|
`);
|
|
205
223
|
await pool.query(`
|
|
206
224
|
CREATE TABLE IF NOT EXISTS _dyrected_ai_threads (
|
|
207
|
-
id VARCHAR(
|
|
225
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
208
226
|
project_id VARCHAR(255) NOT NULL,
|
|
209
227
|
user_id VARCHAR(255) NOT NULL,
|
|
210
228
|
title TEXT,
|
|
@@ -215,8 +233,8 @@ FIX INSTRUCTIONS:
|
|
|
215
233
|
`);
|
|
216
234
|
await pool.query(`
|
|
217
235
|
CREATE TABLE IF NOT EXISTS _dyrected_ai_messages (
|
|
218
|
-
id VARCHAR(
|
|
219
|
-
thread_id VARCHAR(
|
|
236
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
237
|
+
thread_id VARCHAR(191) NOT NULL,
|
|
220
238
|
role VARCHAR(50) NOT NULL,
|
|
221
239
|
content LONGTEXT NOT NULL,
|
|
222
240
|
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
@@ -240,12 +258,22 @@ FIX INSTRUCTIONS:
|
|
|
240
258
|
throw error;
|
|
241
259
|
}
|
|
242
260
|
}
|
|
261
|
+
handleDatabaseError(err) {
|
|
262
|
+
if (err && (err.errno === 1062 || err.code === "ER_DUP_ENTRY" || err.message?.includes("Duplicate entry"))) {
|
|
263
|
+
const { field, value } = parseMysqlDuplicateKey(err.message || "");
|
|
264
|
+
throw new import_core.DuplicateKeyError(
|
|
265
|
+
`Duplicate entry${value ? ` '${value}'` : ""} for unique key${field ? ` '${field}'` : ""}`,
|
|
266
|
+
{ field, value }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
this.handleConnectionError(err);
|
|
270
|
+
}
|
|
243
271
|
async query(sql, params) {
|
|
244
272
|
await this.ensureInitialized();
|
|
245
273
|
try {
|
|
246
274
|
return await this.pool.query(sql, params);
|
|
247
275
|
} catch (err) {
|
|
248
|
-
this.
|
|
276
|
+
this.handleDatabaseError(err);
|
|
249
277
|
}
|
|
250
278
|
}
|
|
251
279
|
async execute(sql, params) {
|
|
@@ -253,7 +281,7 @@ FIX INSTRUCTIONS:
|
|
|
253
281
|
try {
|
|
254
282
|
return await this.pool.execute(sql, params);
|
|
255
283
|
} catch (err) {
|
|
256
|
-
this.
|
|
284
|
+
this.handleDatabaseError(err);
|
|
257
285
|
}
|
|
258
286
|
}
|
|
259
287
|
getTableName(slug) {
|
|
@@ -271,7 +299,7 @@ FIX INSTRUCTIONS:
|
|
|
271
299
|
this.tableColumnsCache.set(tableName, existingCols);
|
|
272
300
|
return existingCols;
|
|
273
301
|
}
|
|
274
|
-
async ensureTable(slug, fields = []) {
|
|
302
|
+
async ensureTable(slug, fields = [], indexes = []) {
|
|
275
303
|
if (slug === "_dyrected_ai_threads" || slug === "_dyrected_ai_messages") {
|
|
276
304
|
return;
|
|
277
305
|
}
|
|
@@ -295,20 +323,39 @@ FIX INSTRUCTIONS:
|
|
|
295
323
|
const tableName = this.getTableName(slug);
|
|
296
324
|
await this.query(`
|
|
297
325
|
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
|
298
|
-
id VARCHAR(
|
|
326
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
299
327
|
data JSON NOT NULL,
|
|
300
328
|
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
301
329
|
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
|
302
330
|
)
|
|
303
331
|
`);
|
|
304
332
|
const existingCols = await this.getTableColumns(tableName);
|
|
333
|
+
try {
|
|
334
|
+
const [idCols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = 'id'`);
|
|
335
|
+
const idCol = idCols?.[0];
|
|
336
|
+
if (idCol && typeof idCol.Type === "string" && idCol.Type.toLowerCase().includes("varchar(36)")) {
|
|
337
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN id VARCHAR(191) NOT NULL`);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
const indexedFields = /* @__PURE__ */ new Set();
|
|
342
|
+
if (Array.isArray(indexes)) {
|
|
343
|
+
for (const idx of indexes) {
|
|
344
|
+
if (Array.isArray(idx.fields)) {
|
|
345
|
+
for (const f of idx.fields) indexedFields.add(f);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
305
349
|
for (const field of fields) {
|
|
306
|
-
|
|
350
|
+
const shouldPromote = field.promoted || field.unique || indexedFields.has(field.name);
|
|
351
|
+
if (shouldPromote && !existingCols.includes(field.name)) {
|
|
307
352
|
console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
|
|
308
|
-
|
|
353
|
+
const isUniqueOrIndexed = field.unique || indexedFields.has(field.name);
|
|
354
|
+
let sqlType = isUniqueOrIndexed ? "VARCHAR(191)" : "TEXT";
|
|
309
355
|
if (field.type === "number") sqlType = "DECIMAL(19,4)";
|
|
310
356
|
if (field.type === "boolean") sqlType = "TINYINT(1)";
|
|
311
357
|
if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
|
|
358
|
+
if (field.type === "textarea" || field.type === "richText" || field.type === "json") sqlType = "LONGTEXT";
|
|
312
359
|
try {
|
|
313
360
|
await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
|
|
314
361
|
existingCols.push(field.name);
|
|
@@ -336,6 +383,75 @@ FIX INSTRUCTIONS:
|
|
|
336
383
|
}
|
|
337
384
|
}
|
|
338
385
|
}
|
|
386
|
+
const rawIndices = await this.query(`SHOW INDEX FROM \`${tableName}\``);
|
|
387
|
+
const existingIndicesRows = Array.isArray(rawIndices?.[0]) ? rawIndices[0] : Array.isArray(rawIndices) ? rawIndices : [];
|
|
388
|
+
const existingIndexNames = new Set(existingIndicesRows.map((r) => r?.Key_name).filter(Boolean));
|
|
389
|
+
for (const field of fields) {
|
|
390
|
+
if (field.unique) {
|
|
391
|
+
const idxName = `uniq_${tableName}_${field.name}`;
|
|
392
|
+
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name) && !this.failedIndexes.has(idxName)) {
|
|
393
|
+
try {
|
|
394
|
+
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [field.name]);
|
|
395
|
+
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
396
|
+
if (colType.includes("text")) {
|
|
397
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${field.name}\` VARCHAR(191)`);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
await this.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE INDEX \`${idxName}\` (\`${field.name}\`)`);
|
|
403
|
+
existingIndexNames.add(idxName);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
406
|
+
this.failedIndexes.add(idxName);
|
|
407
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
408
|
+
console.warn(
|
|
409
|
+
`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (Array.isArray(indexes)) {
|
|
420
|
+
for (const idx of indexes) {
|
|
421
|
+
if (!Array.isArray(idx.fields) || idx.fields.length === 0) continue;
|
|
422
|
+
const isUnique = Boolean(idx.unique);
|
|
423
|
+
const idxName = idx.name || `${isUnique ? "uniq" : "idx"}_${tableName}_${idx.fields.join("_")}`;
|
|
424
|
+
if (!existingIndexNames.has(idxName) && !this.failedIndexes.has(idxName)) {
|
|
425
|
+
for (const fName of idx.fields) {
|
|
426
|
+
try {
|
|
427
|
+
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [fName]);
|
|
428
|
+
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
429
|
+
if (colType.includes("text")) {
|
|
430
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${fName}\` VARCHAR(191)`);
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const uniqueKeyword = isUnique ? "UNIQUE " : "";
|
|
436
|
+
const colList = idx.fields.map((f) => `\`${f.replace(/`/g, "``")}\``).join(", ");
|
|
437
|
+
try {
|
|
438
|
+
await this.query(`ALTER TABLE \`${tableName}\` ADD ${uniqueKeyword}INDEX \`${idxName}\` (${colList})`);
|
|
439
|
+
existingIndexNames.add(idxName);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
442
|
+
this.failedIndexes.add(idxName);
|
|
443
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
444
|
+
console.warn(
|
|
445
|
+
`[dyrected/mysql] Could not create ${isUnique ? "unique " : ""}index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
446
|
+
);
|
|
447
|
+
} else {
|
|
448
|
+
console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
339
455
|
this.ensuredTables.add(slug);
|
|
340
456
|
resolveLock();
|
|
341
457
|
} catch (err) {
|
|
@@ -358,8 +474,9 @@ FIX INSTRUCTIONS:
|
|
|
358
474
|
const result = (0, import_core.parseSqlWhere)(
|
|
359
475
|
args.where,
|
|
360
476
|
(field) => {
|
|
361
|
-
if (field === "
|
|
362
|
-
if (field === "
|
|
477
|
+
if (field === "id") return "`id`";
|
|
478
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
479
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
363
480
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
364
481
|
return `\`${field}\``;
|
|
365
482
|
}
|
|
@@ -371,21 +488,32 @@ FIX INSTRUCTIONS:
|
|
|
371
488
|
whereParams = result.params;
|
|
372
489
|
}
|
|
373
490
|
const sort = normalizeMysqlSort(args.sort, existingCols);
|
|
491
|
+
const lock = this.inTransaction || args.lock === "for-update" ? " FOR UPDATE" : "";
|
|
374
492
|
const [countRows] = await this.query(
|
|
375
493
|
`SELECT COUNT(*) AS total FROM \`${tableName}\` ${whereSql}`,
|
|
376
494
|
whereParams
|
|
377
495
|
);
|
|
378
496
|
const total = Number(countRows[0].total);
|
|
379
497
|
const [rows] = await this.query(
|
|
380
|
-
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET
|
|
498
|
+
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET ?${lock}`,
|
|
381
499
|
[...whereParams, limit, offset]
|
|
382
500
|
);
|
|
383
|
-
const docs = rows.map((r) =>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
501
|
+
const docs = rows.map((r) => {
|
|
502
|
+
const parsedData = JSON.parse(typeof r.data === "string" ? r.data : JSON.stringify(r.data));
|
|
503
|
+
const promotedOverlay = {};
|
|
504
|
+
for (const col of existingCols) {
|
|
505
|
+
if (!["id", "data", "created_at", "updated_at"].includes(col) && r[col] !== void 0 && r[col] !== null) {
|
|
506
|
+
promotedOverlay[col] = r[col];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
id: r.id,
|
|
511
|
+
...parsedData,
|
|
512
|
+
...promotedOverlay,
|
|
513
|
+
createdAt: r.created_at,
|
|
514
|
+
updatedAt: r.updated_at
|
|
515
|
+
};
|
|
516
|
+
});
|
|
389
517
|
const totalPages = Math.ceil(total / limit);
|
|
390
518
|
return {
|
|
391
519
|
docs,
|
|
@@ -400,13 +528,22 @@ FIX INSTRUCTIONS:
|
|
|
400
528
|
async findOne(params) {
|
|
401
529
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
402
530
|
const tableName = this.getTableName(params.collection);
|
|
403
|
-
const lock = this.inTransaction ? " FOR UPDATE" : "";
|
|
531
|
+
const lock = this.inTransaction || params.lock === "for-update" ? " FOR UPDATE" : "";
|
|
404
532
|
const [rows] = await this.query(`SELECT * FROM \`${tableName}\` WHERE id = ?${lock}`, [params.id]);
|
|
405
533
|
const row = rows[0];
|
|
406
534
|
if (!row) return null;
|
|
535
|
+
const parsedData = JSON.parse(typeof row.data === "string" ? row.data : JSON.stringify(row.data));
|
|
536
|
+
const existingCols = await this.getTableColumns(tableName);
|
|
537
|
+
const promotedOverlay = {};
|
|
538
|
+
for (const col of existingCols) {
|
|
539
|
+
if (!["id", "data", "created_at", "updated_at"].includes(col) && row[col] !== void 0 && row[col] !== null) {
|
|
540
|
+
promotedOverlay[col] = row[col];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
407
543
|
return {
|
|
408
544
|
id: row.id,
|
|
409
|
-
...
|
|
545
|
+
...parsedData,
|
|
546
|
+
...promotedOverlay,
|
|
410
547
|
createdAt: row.created_at,
|
|
411
548
|
updatedAt: row.updated_at
|
|
412
549
|
};
|
|
@@ -415,7 +552,8 @@ FIX INSTRUCTIONS:
|
|
|
415
552
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
416
553
|
const tableName = this.getTableName(params.collection);
|
|
417
554
|
const existingCols = await this.getTableColumns(tableName);
|
|
418
|
-
const
|
|
555
|
+
const colConfig = this.collectionConfigs.get(params.collection);
|
|
556
|
+
const id = params.data.id ?? (0, import_core.generateDocumentId)(colConfig || params.collection);
|
|
419
557
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
|
|
420
558
|
const data = { ...params.data };
|
|
421
559
|
delete data.id;
|
|
@@ -432,33 +570,115 @@ FIX INSTRUCTIONS:
|
|
|
432
570
|
const placeholders = colNames.map(() => "?").join(", ");
|
|
433
571
|
const values = [id, JSON.stringify(data), now, now, ...Object.values(promotedValues)];
|
|
434
572
|
await this.query(`INSERT INTO \`${tableName}\` (${colNames.join(", ")}) VALUES (${placeholders})`, values);
|
|
435
|
-
return { id, ...data, createdAt: now, updatedAt: now };
|
|
573
|
+
return { id, ...data, ...promotedValues, createdAt: now, updatedAt: now };
|
|
436
574
|
}
|
|
437
575
|
async update(params) {
|
|
438
576
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
439
577
|
const tableName = this.getTableName(params.collection);
|
|
440
578
|
const existingCols = await this.getTableColumns(tableName);
|
|
441
|
-
const existing = await this.findOne({ collection: params.collection, id: params.id });
|
|
442
579
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
580
|
+
let whereSql = "";
|
|
581
|
+
let whereParams = [];
|
|
582
|
+
if (params.id) {
|
|
583
|
+
whereSql = "WHERE id = ?";
|
|
584
|
+
whereParams = [params.id];
|
|
585
|
+
if (params.where && Object.keys(params.where).length > 0) {
|
|
586
|
+
const parsed = (0, import_core.parseSqlWhere)(
|
|
587
|
+
params.where,
|
|
588
|
+
(field) => {
|
|
589
|
+
if (field === "id") return "`id`";
|
|
590
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
591
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
592
|
+
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
593
|
+
return `\`${field}\``;
|
|
594
|
+
}
|
|
595
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
|
|
596
|
+
},
|
|
597
|
+
"?"
|
|
598
|
+
);
|
|
599
|
+
whereSql += ` AND (${parsed.sql})`;
|
|
600
|
+
whereParams.push(...parsed.params);
|
|
601
|
+
}
|
|
602
|
+
} else if (params.where && Object.keys(params.where).length > 0) {
|
|
603
|
+
const parsed = (0, import_core.parseSqlWhere)(
|
|
604
|
+
params.where,
|
|
605
|
+
(field) => {
|
|
606
|
+
if (field === "id") return "`id`";
|
|
607
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
608
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
609
|
+
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
610
|
+
return `\`${field}\``;
|
|
611
|
+
}
|
|
612
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
|
|
613
|
+
},
|
|
614
|
+
"?"
|
|
615
|
+
);
|
|
616
|
+
whereSql = `WHERE ${parsed.sql}`;
|
|
617
|
+
whereParams = parsed.params;
|
|
618
|
+
} else {
|
|
619
|
+
throw new Error("update requires either id or where clause");
|
|
620
|
+
}
|
|
621
|
+
const setClauses = [];
|
|
622
|
+
const setParams = [];
|
|
623
|
+
const jsonPaths = [];
|
|
624
|
+
const jsonParams = [];
|
|
625
|
+
for (const [key, val] of Object.entries(params.data)) {
|
|
626
|
+
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
627
|
+
const isPromoted = existingCols.includes(key) && !["id", "data", "created_at", "updated_at"].includes(key);
|
|
628
|
+
if (isNumericOp(val)) {
|
|
629
|
+
const delta = Number((val.increment ?? 0) - (val.decrement ?? 0));
|
|
630
|
+
if (isPromoted) {
|
|
631
|
+
const escapedCol = escapeMysqlIdentifier(key);
|
|
632
|
+
setClauses.push(`${escapedCol} = COALESCE(${escapedCol}, 0) + ?`);
|
|
633
|
+
setParams.push(delta);
|
|
634
|
+
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
635
|
+
} else {
|
|
636
|
+
jsonPaths.push(`'$.${key}', CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${key}')), 0) AS DECIMAL(19,4)) + ?`);
|
|
637
|
+
jsonParams.push(delta);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
if (isPromoted) {
|
|
641
|
+
const escapedCol = escapeMysqlIdentifier(key);
|
|
642
|
+
setClauses.push(`${escapedCol} = ?`);
|
|
643
|
+
setParams.push(val);
|
|
644
|
+
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
645
|
+
} else if (val === null) {
|
|
646
|
+
jsonPaths.push(`'$.${key}', CAST('null' AS JSON)`);
|
|
647
|
+
} else if (typeof val === "object") {
|
|
648
|
+
jsonPaths.push(`'$.${key}', CAST(? AS JSON)`);
|
|
649
|
+
jsonParams.push(JSON.stringify(val));
|
|
650
|
+
} else {
|
|
651
|
+
jsonPaths.push(`'$.${key}', ?`);
|
|
652
|
+
jsonParams.push(val);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
setClauses.push("updated_at = ?");
|
|
657
|
+
setParams.push(now);
|
|
658
|
+
if (jsonPaths.length > 0) {
|
|
659
|
+
setClauses.push(`data = JSON_SET(COALESCE(data, '{}'), ${jsonPaths.join(", ")})`);
|
|
660
|
+
setParams.push(...jsonParams);
|
|
661
|
+
}
|
|
662
|
+
const updateSql = `UPDATE \`${tableName}\` SET ${setClauses.join(", ")} ${whereSql}`;
|
|
663
|
+
const [res] = await this.query(updateSql, [...setParams, ...whereParams]);
|
|
664
|
+
let targetId = params.id;
|
|
665
|
+
if (!targetId && params.where) {
|
|
666
|
+
const found = await this.find({ collection: params.collection, where: params.where, limit: 1 });
|
|
667
|
+
targetId = found.docs[0]?.id;
|
|
668
|
+
}
|
|
669
|
+
if (targetId) {
|
|
670
|
+
const doc = await this.findOne({ collection: params.collection, id: targetId });
|
|
671
|
+
if (doc) {
|
|
672
|
+
doc.affectedRows = res?.affectedRows ?? 0;
|
|
673
|
+
return doc;
|
|
452
674
|
}
|
|
453
675
|
}
|
|
454
|
-
|
|
455
|
-
const values = [JSON.stringify(merged), now, ...Object.values(promotedValues), params.id];
|
|
456
|
-
await this.query(`UPDATE \`${tableName}\` SET ${setClauses.join(", ")} WHERE id = ?`, values);
|
|
457
|
-
return { id: params.id, ...merged, createdAt: existing?.createdAt, updatedAt: now };
|
|
676
|
+
return { id: targetId, ...params.data, affectedRows: res?.affectedRows ?? 0, updatedAt: now };
|
|
458
677
|
}
|
|
459
678
|
async sync(collections) {
|
|
460
679
|
for (const col of collections) {
|
|
461
|
-
|
|
680
|
+
this.collectionConfigs.set(col.slug, col);
|
|
681
|
+
await this.ensureTable(col.slug, col.fields, col.indexes);
|
|
462
682
|
}
|
|
463
683
|
}
|
|
464
684
|
async delete(params) {
|
|
@@ -525,7 +745,7 @@ FIX INSTRUCTIONS:
|
|
|
525
745
|
const base = toFieldExpr(rawField);
|
|
526
746
|
if (cast === "string") return base;
|
|
527
747
|
if (cast === "boolean") return `IF(${base} IS NOT NULL, IF(${base} IN ('true','1'), 1, 0), NULL)`;
|
|
528
|
-
if (cast === "date") return `CAST(${base} AS DATETIME)`;
|
|
748
|
+
if (cast === "date") return `IF(${base} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}', CAST(${base} AS DATETIME), NULL)`;
|
|
529
749
|
const sqlType = cast === "integer" ? "SIGNED" : "DECIMAL(20,6)";
|
|
530
750
|
return `IF(${base} REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(${base} AS ${sqlType}), NULL)`;
|
|
531
751
|
};
|
|
@@ -595,11 +815,16 @@ FIX INSTRUCTIONS:
|
|
|
595
815
|
const result = {};
|
|
596
816
|
for (const name of Object.keys(args.aggregates)) {
|
|
597
817
|
const raw = row[name];
|
|
818
|
+
const op = args.aggregates[name];
|
|
598
819
|
if (isDistinctMap[name]) {
|
|
599
820
|
const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
|
|
600
821
|
result[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
|
|
822
|
+
} else if (raw instanceof Date) {
|
|
823
|
+
result[name] = raw.toISOString();
|
|
824
|
+
} else if (op?.cast === "date") {
|
|
825
|
+
result[name] = raw === null || raw === void 0 ? null : String(raw);
|
|
601
826
|
} else {
|
|
602
|
-
result[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
827
|
+
result[name] = raw === null || raw === void 0 ? null : isNaN(Number(raw)) ? raw : Number(raw);
|
|
603
828
|
}
|
|
604
829
|
}
|
|
605
830
|
return result;
|
|
@@ -609,6 +834,7 @@ FIX INSTRUCTIONS:
|
|
|
609
834
|
const cacheKey = getMysqlCacheKey(this.config);
|
|
610
835
|
cache.delete(cacheKey);
|
|
611
836
|
this.ensuredTables.clear();
|
|
837
|
+
this.failedIndexes.clear();
|
|
612
838
|
this.tableColumnsCache.clear();
|
|
613
839
|
if (this.initPromise) {
|
|
614
840
|
try {
|
package/dist/index.d.cts
CHANGED
|
@@ -19,12 +19,15 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
19
19
|
private inTransaction;
|
|
20
20
|
private tableLocks;
|
|
21
21
|
private ensuredTables;
|
|
22
|
+
private failedIndexes;
|
|
22
23
|
private tableColumnsCache;
|
|
24
|
+
private collectionConfigs;
|
|
23
25
|
constructor(config: MysqlAdapterConfig);
|
|
24
26
|
private handleConnectionError;
|
|
25
27
|
private retryOperation;
|
|
26
28
|
private ensureInitialized;
|
|
27
29
|
private initialize;
|
|
30
|
+
private handleDatabaseError;
|
|
28
31
|
private query;
|
|
29
32
|
execute(sql: string, params?: any[]): Promise<any>;
|
|
30
33
|
private getTableName;
|
|
@@ -36,10 +39,13 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
36
39
|
limit?: number;
|
|
37
40
|
page?: number;
|
|
38
41
|
sort?: string;
|
|
42
|
+
fields?: any[];
|
|
43
|
+
lock?: "for-update";
|
|
39
44
|
}): Promise<PaginatedResult>;
|
|
40
45
|
findOne(params: {
|
|
41
46
|
collection: string;
|
|
42
47
|
id: string;
|
|
48
|
+
lock?: "for-update";
|
|
43
49
|
}): Promise<any>;
|
|
44
50
|
create(params: {
|
|
45
51
|
collection: string;
|
|
@@ -47,7 +53,8 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
47
53
|
}): Promise<any>;
|
|
48
54
|
update(params: {
|
|
49
55
|
collection: string;
|
|
50
|
-
id
|
|
56
|
+
id?: string;
|
|
57
|
+
where?: any;
|
|
51
58
|
data: any;
|
|
52
59
|
}): Promise<any>;
|
|
53
60
|
sync(collections: any[]): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,12 +19,15 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
19
19
|
private inTransaction;
|
|
20
20
|
private tableLocks;
|
|
21
21
|
private ensuredTables;
|
|
22
|
+
private failedIndexes;
|
|
22
23
|
private tableColumnsCache;
|
|
24
|
+
private collectionConfigs;
|
|
23
25
|
constructor(config: MysqlAdapterConfig);
|
|
24
26
|
private handleConnectionError;
|
|
25
27
|
private retryOperation;
|
|
26
28
|
private ensureInitialized;
|
|
27
29
|
private initialize;
|
|
30
|
+
private handleDatabaseError;
|
|
28
31
|
private query;
|
|
29
32
|
execute(sql: string, params?: any[]): Promise<any>;
|
|
30
33
|
private getTableName;
|
|
@@ -36,10 +39,13 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
36
39
|
limit?: number;
|
|
37
40
|
page?: number;
|
|
38
41
|
sort?: string;
|
|
42
|
+
fields?: any[];
|
|
43
|
+
lock?: "for-update";
|
|
39
44
|
}): Promise<PaginatedResult>;
|
|
40
45
|
findOne(params: {
|
|
41
46
|
collection: string;
|
|
42
47
|
id: string;
|
|
48
|
+
lock?: "for-update";
|
|
43
49
|
}): Promise<any>;
|
|
44
50
|
create(params: {
|
|
45
51
|
collection: string;
|
|
@@ -47,7 +53,8 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
47
53
|
}): Promise<any>;
|
|
48
54
|
update(params: {
|
|
49
55
|
collection: string;
|
|
50
|
-
id
|
|
56
|
+
id?: string;
|
|
57
|
+
where?: any;
|
|
51
58
|
data: any;
|
|
52
59
|
}): Promise<any>;
|
|
53
60
|
sync(collections: any[]): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { parseSort, parseSqlWhere } from "@dyrected/core";
|
|
2
|
+
import { parseSort, parseSqlWhere, DuplicateKeyError, generateDocumentId } from "@dyrected/core";
|
|
3
3
|
import mysql from "mysql2/promise";
|
|
4
|
+
function parseMysqlDuplicateKey(message) {
|
|
5
|
+
const match = message.match(/Duplicate entry '(.*)' for key '(?:[^.]*\.)?(.*)'/);
|
|
6
|
+
if (!match) return {};
|
|
7
|
+
const [, value, keyName] = match;
|
|
8
|
+
let field = keyName;
|
|
9
|
+
if (keyName === "PRIMARY") {
|
|
10
|
+
field = "id";
|
|
11
|
+
} else if (keyName.startsWith("uniq_") || keyName.startsWith("idx_")) {
|
|
12
|
+
const parts = keyName.split("_");
|
|
13
|
+
field = parts[parts.length - 1];
|
|
14
|
+
}
|
|
15
|
+
return { field, value };
|
|
16
|
+
}
|
|
17
|
+
function isNumericOp(val) {
|
|
18
|
+
return val !== null && typeof val === "object" && !Array.isArray(val) && ("increment" in val || "decrement" in val);
|
|
19
|
+
}
|
|
4
20
|
var MYSQL_CLIENT_CACHE_KEY = "__dyrectedMysqlClientCache";
|
|
5
21
|
function getSharedMysqlClientCache() {
|
|
6
22
|
const globalScope = globalThis;
|
|
@@ -33,7 +49,9 @@ var MysqlAdapter = class {
|
|
|
33
49
|
inTransaction = false;
|
|
34
50
|
tableLocks = /* @__PURE__ */ new Map();
|
|
35
51
|
ensuredTables = /* @__PURE__ */ new Set();
|
|
52
|
+
failedIndexes = /* @__PURE__ */ new Set();
|
|
36
53
|
tableColumnsCache = /* @__PURE__ */ new Map();
|
|
54
|
+
collectionConfigs = /* @__PURE__ */ new Map();
|
|
37
55
|
constructor(config) {
|
|
38
56
|
this.config = config;
|
|
39
57
|
}
|
|
@@ -168,7 +186,7 @@ FIX INSTRUCTIONS:
|
|
|
168
186
|
`);
|
|
169
187
|
await pool.query(`
|
|
170
188
|
CREATE TABLE IF NOT EXISTS _dyrected_ai_threads (
|
|
171
|
-
id VARCHAR(
|
|
189
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
172
190
|
project_id VARCHAR(255) NOT NULL,
|
|
173
191
|
user_id VARCHAR(255) NOT NULL,
|
|
174
192
|
title TEXT,
|
|
@@ -179,8 +197,8 @@ FIX INSTRUCTIONS:
|
|
|
179
197
|
`);
|
|
180
198
|
await pool.query(`
|
|
181
199
|
CREATE TABLE IF NOT EXISTS _dyrected_ai_messages (
|
|
182
|
-
id VARCHAR(
|
|
183
|
-
thread_id VARCHAR(
|
|
200
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
201
|
+
thread_id VARCHAR(191) NOT NULL,
|
|
184
202
|
role VARCHAR(50) NOT NULL,
|
|
185
203
|
content LONGTEXT NOT NULL,
|
|
186
204
|
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
@@ -204,12 +222,22 @@ FIX INSTRUCTIONS:
|
|
|
204
222
|
throw error;
|
|
205
223
|
}
|
|
206
224
|
}
|
|
225
|
+
handleDatabaseError(err) {
|
|
226
|
+
if (err && (err.errno === 1062 || err.code === "ER_DUP_ENTRY" || err.message?.includes("Duplicate entry"))) {
|
|
227
|
+
const { field, value } = parseMysqlDuplicateKey(err.message || "");
|
|
228
|
+
throw new DuplicateKeyError(
|
|
229
|
+
`Duplicate entry${value ? ` '${value}'` : ""} for unique key${field ? ` '${field}'` : ""}`,
|
|
230
|
+
{ field, value }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
this.handleConnectionError(err);
|
|
234
|
+
}
|
|
207
235
|
async query(sql, params) {
|
|
208
236
|
await this.ensureInitialized();
|
|
209
237
|
try {
|
|
210
238
|
return await this.pool.query(sql, params);
|
|
211
239
|
} catch (err) {
|
|
212
|
-
this.
|
|
240
|
+
this.handleDatabaseError(err);
|
|
213
241
|
}
|
|
214
242
|
}
|
|
215
243
|
async execute(sql, params) {
|
|
@@ -217,7 +245,7 @@ FIX INSTRUCTIONS:
|
|
|
217
245
|
try {
|
|
218
246
|
return await this.pool.execute(sql, params);
|
|
219
247
|
} catch (err) {
|
|
220
|
-
this.
|
|
248
|
+
this.handleDatabaseError(err);
|
|
221
249
|
}
|
|
222
250
|
}
|
|
223
251
|
getTableName(slug) {
|
|
@@ -235,7 +263,7 @@ FIX INSTRUCTIONS:
|
|
|
235
263
|
this.tableColumnsCache.set(tableName, existingCols);
|
|
236
264
|
return existingCols;
|
|
237
265
|
}
|
|
238
|
-
async ensureTable(slug, fields = []) {
|
|
266
|
+
async ensureTable(slug, fields = [], indexes = []) {
|
|
239
267
|
if (slug === "_dyrected_ai_threads" || slug === "_dyrected_ai_messages") {
|
|
240
268
|
return;
|
|
241
269
|
}
|
|
@@ -259,20 +287,39 @@ FIX INSTRUCTIONS:
|
|
|
259
287
|
const tableName = this.getTableName(slug);
|
|
260
288
|
await this.query(`
|
|
261
289
|
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
|
262
|
-
id VARCHAR(
|
|
290
|
+
id VARCHAR(191) PRIMARY KEY,
|
|
263
291
|
data JSON NOT NULL,
|
|
264
292
|
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
265
293
|
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
|
266
294
|
)
|
|
267
295
|
`);
|
|
268
296
|
const existingCols = await this.getTableColumns(tableName);
|
|
297
|
+
try {
|
|
298
|
+
const [idCols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = 'id'`);
|
|
299
|
+
const idCol = idCols?.[0];
|
|
300
|
+
if (idCol && typeof idCol.Type === "string" && idCol.Type.toLowerCase().includes("varchar(36)")) {
|
|
301
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN id VARCHAR(191) NOT NULL`);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
const indexedFields = /* @__PURE__ */ new Set();
|
|
306
|
+
if (Array.isArray(indexes)) {
|
|
307
|
+
for (const idx of indexes) {
|
|
308
|
+
if (Array.isArray(idx.fields)) {
|
|
309
|
+
for (const f of idx.fields) indexedFields.add(f);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
269
313
|
for (const field of fields) {
|
|
270
|
-
|
|
314
|
+
const shouldPromote = field.promoted || field.unique || indexedFields.has(field.name);
|
|
315
|
+
if (shouldPromote && !existingCols.includes(field.name)) {
|
|
271
316
|
console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
|
|
272
|
-
|
|
317
|
+
const isUniqueOrIndexed = field.unique || indexedFields.has(field.name);
|
|
318
|
+
let sqlType = isUniqueOrIndexed ? "VARCHAR(191)" : "TEXT";
|
|
273
319
|
if (field.type === "number") sqlType = "DECIMAL(19,4)";
|
|
274
320
|
if (field.type === "boolean") sqlType = "TINYINT(1)";
|
|
275
321
|
if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
|
|
322
|
+
if (field.type === "textarea" || field.type === "richText" || field.type === "json") sqlType = "LONGTEXT";
|
|
276
323
|
try {
|
|
277
324
|
await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
|
|
278
325
|
existingCols.push(field.name);
|
|
@@ -300,6 +347,75 @@ FIX INSTRUCTIONS:
|
|
|
300
347
|
}
|
|
301
348
|
}
|
|
302
349
|
}
|
|
350
|
+
const rawIndices = await this.query(`SHOW INDEX FROM \`${tableName}\``);
|
|
351
|
+
const existingIndicesRows = Array.isArray(rawIndices?.[0]) ? rawIndices[0] : Array.isArray(rawIndices) ? rawIndices : [];
|
|
352
|
+
const existingIndexNames = new Set(existingIndicesRows.map((r) => r?.Key_name).filter(Boolean));
|
|
353
|
+
for (const field of fields) {
|
|
354
|
+
if (field.unique) {
|
|
355
|
+
const idxName = `uniq_${tableName}_${field.name}`;
|
|
356
|
+
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name) && !this.failedIndexes.has(idxName)) {
|
|
357
|
+
try {
|
|
358
|
+
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [field.name]);
|
|
359
|
+
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
360
|
+
if (colType.includes("text")) {
|
|
361
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${field.name}\` VARCHAR(191)`);
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await this.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE INDEX \`${idxName}\` (\`${field.name}\`)`);
|
|
367
|
+
existingIndexNames.add(idxName);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
370
|
+
this.failedIndexes.add(idxName);
|
|
371
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
372
|
+
console.warn(
|
|
373
|
+
`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(indexes)) {
|
|
384
|
+
for (const idx of indexes) {
|
|
385
|
+
if (!Array.isArray(idx.fields) || idx.fields.length === 0) continue;
|
|
386
|
+
const isUnique = Boolean(idx.unique);
|
|
387
|
+
const idxName = idx.name || `${isUnique ? "uniq" : "idx"}_${tableName}_${idx.fields.join("_")}`;
|
|
388
|
+
if (!existingIndexNames.has(idxName) && !this.failedIndexes.has(idxName)) {
|
|
389
|
+
for (const fName of idx.fields) {
|
|
390
|
+
try {
|
|
391
|
+
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [fName]);
|
|
392
|
+
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
393
|
+
if (colType.includes("text")) {
|
|
394
|
+
await this.query(`ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${fName}\` VARCHAR(191)`);
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const uniqueKeyword = isUnique ? "UNIQUE " : "";
|
|
400
|
+
const colList = idx.fields.map((f) => `\`${f.replace(/`/g, "``")}\``).join(", ");
|
|
401
|
+
try {
|
|
402
|
+
await this.query(`ALTER TABLE \`${tableName}\` ADD ${uniqueKeyword}INDEX \`${idxName}\` (${colList})`);
|
|
403
|
+
existingIndexNames.add(idxName);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
406
|
+
this.failedIndexes.add(idxName);
|
|
407
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
408
|
+
console.warn(
|
|
409
|
+
`[dyrected/mysql] Could not create ${isUnique ? "unique " : ""}index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
303
419
|
this.ensuredTables.add(slug);
|
|
304
420
|
resolveLock();
|
|
305
421
|
} catch (err) {
|
|
@@ -322,8 +438,9 @@ FIX INSTRUCTIONS:
|
|
|
322
438
|
const result = parseSqlWhere(
|
|
323
439
|
args.where,
|
|
324
440
|
(field) => {
|
|
325
|
-
if (field === "
|
|
326
|
-
if (field === "
|
|
441
|
+
if (field === "id") return "`id`";
|
|
442
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
443
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
327
444
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
328
445
|
return `\`${field}\``;
|
|
329
446
|
}
|
|
@@ -335,21 +452,32 @@ FIX INSTRUCTIONS:
|
|
|
335
452
|
whereParams = result.params;
|
|
336
453
|
}
|
|
337
454
|
const sort = normalizeMysqlSort(args.sort, existingCols);
|
|
455
|
+
const lock = this.inTransaction || args.lock === "for-update" ? " FOR UPDATE" : "";
|
|
338
456
|
const [countRows] = await this.query(
|
|
339
457
|
`SELECT COUNT(*) AS total FROM \`${tableName}\` ${whereSql}`,
|
|
340
458
|
whereParams
|
|
341
459
|
);
|
|
342
460
|
const total = Number(countRows[0].total);
|
|
343
461
|
const [rows] = await this.query(
|
|
344
|
-
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET
|
|
462
|
+
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET ?${lock}`,
|
|
345
463
|
[...whereParams, limit, offset]
|
|
346
464
|
);
|
|
347
|
-
const docs = rows.map((r) =>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
465
|
+
const docs = rows.map((r) => {
|
|
466
|
+
const parsedData = JSON.parse(typeof r.data === "string" ? r.data : JSON.stringify(r.data));
|
|
467
|
+
const promotedOverlay = {};
|
|
468
|
+
for (const col of existingCols) {
|
|
469
|
+
if (!["id", "data", "created_at", "updated_at"].includes(col) && r[col] !== void 0 && r[col] !== null) {
|
|
470
|
+
promotedOverlay[col] = r[col];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
id: r.id,
|
|
475
|
+
...parsedData,
|
|
476
|
+
...promotedOverlay,
|
|
477
|
+
createdAt: r.created_at,
|
|
478
|
+
updatedAt: r.updated_at
|
|
479
|
+
};
|
|
480
|
+
});
|
|
353
481
|
const totalPages = Math.ceil(total / limit);
|
|
354
482
|
return {
|
|
355
483
|
docs,
|
|
@@ -364,13 +492,22 @@ FIX INSTRUCTIONS:
|
|
|
364
492
|
async findOne(params) {
|
|
365
493
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
366
494
|
const tableName = this.getTableName(params.collection);
|
|
367
|
-
const lock = this.inTransaction ? " FOR UPDATE" : "";
|
|
495
|
+
const lock = this.inTransaction || params.lock === "for-update" ? " FOR UPDATE" : "";
|
|
368
496
|
const [rows] = await this.query(`SELECT * FROM \`${tableName}\` WHERE id = ?${lock}`, [params.id]);
|
|
369
497
|
const row = rows[0];
|
|
370
498
|
if (!row) return null;
|
|
499
|
+
const parsedData = JSON.parse(typeof row.data === "string" ? row.data : JSON.stringify(row.data));
|
|
500
|
+
const existingCols = await this.getTableColumns(tableName);
|
|
501
|
+
const promotedOverlay = {};
|
|
502
|
+
for (const col of existingCols) {
|
|
503
|
+
if (!["id", "data", "created_at", "updated_at"].includes(col) && row[col] !== void 0 && row[col] !== null) {
|
|
504
|
+
promotedOverlay[col] = row[col];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
371
507
|
return {
|
|
372
508
|
id: row.id,
|
|
373
|
-
...
|
|
509
|
+
...parsedData,
|
|
510
|
+
...promotedOverlay,
|
|
374
511
|
createdAt: row.created_at,
|
|
375
512
|
updatedAt: row.updated_at
|
|
376
513
|
};
|
|
@@ -379,7 +516,8 @@ FIX INSTRUCTIONS:
|
|
|
379
516
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
380
517
|
const tableName = this.getTableName(params.collection);
|
|
381
518
|
const existingCols = await this.getTableColumns(tableName);
|
|
382
|
-
const
|
|
519
|
+
const colConfig = this.collectionConfigs.get(params.collection);
|
|
520
|
+
const id = params.data.id ?? generateDocumentId(colConfig || params.collection);
|
|
383
521
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
|
|
384
522
|
const data = { ...params.data };
|
|
385
523
|
delete data.id;
|
|
@@ -396,33 +534,115 @@ FIX INSTRUCTIONS:
|
|
|
396
534
|
const placeholders = colNames.map(() => "?").join(", ");
|
|
397
535
|
const values = [id, JSON.stringify(data), now, now, ...Object.values(promotedValues)];
|
|
398
536
|
await this.query(`INSERT INTO \`${tableName}\` (${colNames.join(", ")}) VALUES (${placeholders})`, values);
|
|
399
|
-
return { id, ...data, createdAt: now, updatedAt: now };
|
|
537
|
+
return { id, ...data, ...promotedValues, createdAt: now, updatedAt: now };
|
|
400
538
|
}
|
|
401
539
|
async update(params) {
|
|
402
540
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
403
541
|
const tableName = this.getTableName(params.collection);
|
|
404
542
|
const existingCols = await this.getTableColumns(tableName);
|
|
405
|
-
const existing = await this.findOne({ collection: params.collection, id: params.id });
|
|
406
543
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
544
|
+
let whereSql = "";
|
|
545
|
+
let whereParams = [];
|
|
546
|
+
if (params.id) {
|
|
547
|
+
whereSql = "WHERE id = ?";
|
|
548
|
+
whereParams = [params.id];
|
|
549
|
+
if (params.where && Object.keys(params.where).length > 0) {
|
|
550
|
+
const parsed = parseSqlWhere(
|
|
551
|
+
params.where,
|
|
552
|
+
(field) => {
|
|
553
|
+
if (field === "id") return "`id`";
|
|
554
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
555
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
556
|
+
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
557
|
+
return `\`${field}\``;
|
|
558
|
+
}
|
|
559
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
|
|
560
|
+
},
|
|
561
|
+
"?"
|
|
562
|
+
);
|
|
563
|
+
whereSql += ` AND (${parsed.sql})`;
|
|
564
|
+
whereParams.push(...parsed.params);
|
|
565
|
+
}
|
|
566
|
+
} else if (params.where && Object.keys(params.where).length > 0) {
|
|
567
|
+
const parsed = parseSqlWhere(
|
|
568
|
+
params.where,
|
|
569
|
+
(field) => {
|
|
570
|
+
if (field === "id") return "`id`";
|
|
571
|
+
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
572
|
+
if (field === "updatedAt" || field === "updated_at") return "`updated_at`";
|
|
573
|
+
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
574
|
+
return `\`${field}\``;
|
|
575
|
+
}
|
|
576
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
|
|
577
|
+
},
|
|
578
|
+
"?"
|
|
579
|
+
);
|
|
580
|
+
whereSql = `WHERE ${parsed.sql}`;
|
|
581
|
+
whereParams = parsed.params;
|
|
582
|
+
} else {
|
|
583
|
+
throw new Error("update requires either id or where clause");
|
|
584
|
+
}
|
|
585
|
+
const setClauses = [];
|
|
586
|
+
const setParams = [];
|
|
587
|
+
const jsonPaths = [];
|
|
588
|
+
const jsonParams = [];
|
|
589
|
+
for (const [key, val] of Object.entries(params.data)) {
|
|
590
|
+
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
591
|
+
const isPromoted = existingCols.includes(key) && !["id", "data", "created_at", "updated_at"].includes(key);
|
|
592
|
+
if (isNumericOp(val)) {
|
|
593
|
+
const delta = Number((val.increment ?? 0) - (val.decrement ?? 0));
|
|
594
|
+
if (isPromoted) {
|
|
595
|
+
const escapedCol = escapeMysqlIdentifier(key);
|
|
596
|
+
setClauses.push(`${escapedCol} = COALESCE(${escapedCol}, 0) + ?`);
|
|
597
|
+
setParams.push(delta);
|
|
598
|
+
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
599
|
+
} else {
|
|
600
|
+
jsonPaths.push(`'$.${key}', CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${key}')), 0) AS DECIMAL(19,4)) + ?`);
|
|
601
|
+
jsonParams.push(delta);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
if (isPromoted) {
|
|
605
|
+
const escapedCol = escapeMysqlIdentifier(key);
|
|
606
|
+
setClauses.push(`${escapedCol} = ?`);
|
|
607
|
+
setParams.push(val);
|
|
608
|
+
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
609
|
+
} else if (val === null) {
|
|
610
|
+
jsonPaths.push(`'$.${key}', CAST('null' AS JSON)`);
|
|
611
|
+
} else if (typeof val === "object") {
|
|
612
|
+
jsonPaths.push(`'$.${key}', CAST(? AS JSON)`);
|
|
613
|
+
jsonParams.push(JSON.stringify(val));
|
|
614
|
+
} else {
|
|
615
|
+
jsonPaths.push(`'$.${key}', ?`);
|
|
616
|
+
jsonParams.push(val);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
setClauses.push("updated_at = ?");
|
|
621
|
+
setParams.push(now);
|
|
622
|
+
if (jsonPaths.length > 0) {
|
|
623
|
+
setClauses.push(`data = JSON_SET(COALESCE(data, '{}'), ${jsonPaths.join(", ")})`);
|
|
624
|
+
setParams.push(...jsonParams);
|
|
625
|
+
}
|
|
626
|
+
const updateSql = `UPDATE \`${tableName}\` SET ${setClauses.join(", ")} ${whereSql}`;
|
|
627
|
+
const [res] = await this.query(updateSql, [...setParams, ...whereParams]);
|
|
628
|
+
let targetId = params.id;
|
|
629
|
+
if (!targetId && params.where) {
|
|
630
|
+
const found = await this.find({ collection: params.collection, where: params.where, limit: 1 });
|
|
631
|
+
targetId = found.docs[0]?.id;
|
|
632
|
+
}
|
|
633
|
+
if (targetId) {
|
|
634
|
+
const doc = await this.findOne({ collection: params.collection, id: targetId });
|
|
635
|
+
if (doc) {
|
|
636
|
+
doc.affectedRows = res?.affectedRows ?? 0;
|
|
637
|
+
return doc;
|
|
416
638
|
}
|
|
417
639
|
}
|
|
418
|
-
|
|
419
|
-
const values = [JSON.stringify(merged), now, ...Object.values(promotedValues), params.id];
|
|
420
|
-
await this.query(`UPDATE \`${tableName}\` SET ${setClauses.join(", ")} WHERE id = ?`, values);
|
|
421
|
-
return { id: params.id, ...merged, createdAt: existing?.createdAt, updatedAt: now };
|
|
640
|
+
return { id: targetId, ...params.data, affectedRows: res?.affectedRows ?? 0, updatedAt: now };
|
|
422
641
|
}
|
|
423
642
|
async sync(collections) {
|
|
424
643
|
for (const col of collections) {
|
|
425
|
-
|
|
644
|
+
this.collectionConfigs.set(col.slug, col);
|
|
645
|
+
await this.ensureTable(col.slug, col.fields, col.indexes);
|
|
426
646
|
}
|
|
427
647
|
}
|
|
428
648
|
async delete(params) {
|
|
@@ -489,7 +709,7 @@ FIX INSTRUCTIONS:
|
|
|
489
709
|
const base = toFieldExpr(rawField);
|
|
490
710
|
if (cast === "string") return base;
|
|
491
711
|
if (cast === "boolean") return `IF(${base} IS NOT NULL, IF(${base} IN ('true','1'), 1, 0), NULL)`;
|
|
492
|
-
if (cast === "date") return `CAST(${base} AS DATETIME)`;
|
|
712
|
+
if (cast === "date") return `IF(${base} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}', CAST(${base} AS DATETIME), NULL)`;
|
|
493
713
|
const sqlType = cast === "integer" ? "SIGNED" : "DECIMAL(20,6)";
|
|
494
714
|
return `IF(${base} REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(${base} AS ${sqlType}), NULL)`;
|
|
495
715
|
};
|
|
@@ -559,11 +779,16 @@ FIX INSTRUCTIONS:
|
|
|
559
779
|
const result = {};
|
|
560
780
|
for (const name of Object.keys(args.aggregates)) {
|
|
561
781
|
const raw = row[name];
|
|
782
|
+
const op = args.aggregates[name];
|
|
562
783
|
if (isDistinctMap[name]) {
|
|
563
784
|
const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
|
|
564
785
|
result[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
|
|
786
|
+
} else if (raw instanceof Date) {
|
|
787
|
+
result[name] = raw.toISOString();
|
|
788
|
+
} else if (op?.cast === "date") {
|
|
789
|
+
result[name] = raw === null || raw === void 0 ? null : String(raw);
|
|
565
790
|
} else {
|
|
566
|
-
result[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
791
|
+
result[name] = raw === null || raw === void 0 ? null : isNaN(Number(raw)) ? raw : Number(raw);
|
|
567
792
|
}
|
|
568
793
|
}
|
|
569
794
|
return result;
|
|
@@ -573,6 +798,7 @@ FIX INSTRUCTIONS:
|
|
|
573
798
|
const cacheKey = getMysqlCacheKey(this.config);
|
|
574
799
|
cache.delete(cacheKey);
|
|
575
800
|
this.ensuredTables.clear();
|
|
801
|
+
this.failedIndexes.clear();
|
|
576
802
|
this.tableColumnsCache.clear();
|
|
577
803
|
if (this.initPromise) {
|
|
578
804
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dyrected/db-mysql",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "MySQL adapter for Dyrected CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"mysql2": "^3.22.3",
|
|
21
|
-
"@dyrected/core": "^2.
|
|
21
|
+
"@dyrected/core": "^2.17.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"tsup": "^8.0.0",
|