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