@dyrected/db-mysql 2.16.0 → 2.18.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 +106 -28
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +107 -29
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -50,6 +50,24 @@ function parseMysqlDuplicateKey(message) {
|
|
|
50
50
|
}
|
|
51
51
|
return { field, value };
|
|
52
52
|
}
|
|
53
|
+
function jsonWhereExpr(field) {
|
|
54
|
+
const path = `JSON_EXTRACT(data, '$.${field}')`;
|
|
55
|
+
return `IF(JSON_TYPE(${path}) = 'NULL', NULL, IF(JSON_TYPE(${path}) = 'BOOLEAN', IF(${path} = TRUE, 1, 0), JSON_UNQUOTE(${path})))`;
|
|
56
|
+
}
|
|
57
|
+
var DATE_TYPES = /* @__PURE__ */ new Set(["date", "datetime"]);
|
|
58
|
+
var ISO_WITH_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
59
|
+
var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
60
|
+
function toDatetimeColumn(value) {
|
|
61
|
+
if (typeof value !== "string" || !ISO_WITH_TIME.test(value)) return value;
|
|
62
|
+
const date = new Date(value);
|
|
63
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
64
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
65
|
+
}
|
|
66
|
+
function fromDatetimeColumn(value) {
|
|
67
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
68
|
+
if (typeof value === "string" && MYSQL_DATETIME.test(value)) return `${value.replace(" ", "T")}Z`;
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
53
71
|
function isNumericOp(val) {
|
|
54
72
|
return val !== null && typeof val === "object" && !Array.isArray(val) && ("increment" in val || "decrement" in val);
|
|
55
73
|
}
|
|
@@ -85,6 +103,7 @@ var MysqlAdapter = class {
|
|
|
85
103
|
inTransaction = false;
|
|
86
104
|
tableLocks = /* @__PURE__ */ new Map();
|
|
87
105
|
ensuredTables = /* @__PURE__ */ new Set();
|
|
106
|
+
failedIndexes = /* @__PURE__ */ new Set();
|
|
88
107
|
tableColumnsCache = /* @__PURE__ */ new Map();
|
|
89
108
|
collectionConfigs = /* @__PURE__ */ new Map();
|
|
90
109
|
constructor(config) {
|
|
@@ -351,7 +370,15 @@ FIX INSTRUCTIONS:
|
|
|
351
370
|
console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
|
|
352
371
|
const isUniqueOrIndexed = field.unique || indexedFields.has(field.name);
|
|
353
372
|
let sqlType = isUniqueOrIndexed ? "VARCHAR(191)" : "TEXT";
|
|
373
|
+
if (!isUniqueOrIndexed) {
|
|
374
|
+
if ((field.type === "select" || field.type === "radio") && !field.hasMany) sqlType = "VARCHAR(100)";
|
|
375
|
+
else if (field.type === "relationship" && !field.hasMany) sqlType = "VARCHAR(191)";
|
|
376
|
+
else if (field.type === "email") sqlType = "VARCHAR(254)";
|
|
377
|
+
} else if (field.type === "email") {
|
|
378
|
+
sqlType = "VARCHAR(191)";
|
|
379
|
+
}
|
|
354
380
|
if (field.type === "number") sqlType = "DECIMAL(19,4)";
|
|
381
|
+
if (field.type === "money") sqlType = "BIGINT";
|
|
355
382
|
if (field.type === "boolean") sqlType = "TINYINT(1)";
|
|
356
383
|
if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
|
|
357
384
|
if (field.type === "textarea" || field.type === "richText" || field.type === "json") sqlType = "LONGTEXT";
|
|
@@ -369,6 +396,8 @@ FIX INSTRUCTIONS:
|
|
|
369
396
|
let castExpr = `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}'))`;
|
|
370
397
|
if (field.type === "number") {
|
|
371
398
|
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) AS DECIMAL(19,4)), NULL)`;
|
|
399
|
+
} else if (field.type === "money") {
|
|
400
|
+
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) REGEXP '^-?[0-9]+$', CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) AS SIGNED), NULL)`;
|
|
372
401
|
} else if (field.type === "boolean") {
|
|
373
402
|
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) IN ('true', '1'), 1, 0)`;
|
|
374
403
|
} else if (field.type === "date" || field.type === "datetime") {
|
|
@@ -387,8 +416,8 @@ FIX INSTRUCTIONS:
|
|
|
387
416
|
const existingIndexNames = new Set(existingIndicesRows.map((r) => r?.Key_name).filter(Boolean));
|
|
388
417
|
for (const field of fields) {
|
|
389
418
|
if (field.unique) {
|
|
390
|
-
const idxName =
|
|
391
|
-
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name)) {
|
|
419
|
+
const idxName = (0, import_core.resolveIndexName)("uniq", tableName, [field.name]);
|
|
420
|
+
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name) && !this.failedIndexes.has(idxName)) {
|
|
392
421
|
try {
|
|
393
422
|
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [field.name]);
|
|
394
423
|
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
@@ -402,7 +431,14 @@ FIX INSTRUCTIONS:
|
|
|
402
431
|
existingIndexNames.add(idxName);
|
|
403
432
|
} catch (err) {
|
|
404
433
|
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
405
|
-
|
|
434
|
+
this.failedIndexes.add(idxName);
|
|
435
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
436
|
+
console.warn(
|
|
437
|
+
`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
|
|
441
|
+
}
|
|
406
442
|
}
|
|
407
443
|
}
|
|
408
444
|
}
|
|
@@ -412,8 +448,8 @@ FIX INSTRUCTIONS:
|
|
|
412
448
|
for (const idx of indexes) {
|
|
413
449
|
if (!Array.isArray(idx.fields) || idx.fields.length === 0) continue;
|
|
414
450
|
const isUnique = Boolean(idx.unique);
|
|
415
|
-
const idxName = idx.name ||
|
|
416
|
-
if (!existingIndexNames.has(idxName)) {
|
|
451
|
+
const idxName = idx.name || (0, import_core.resolveIndexName)(isUnique ? "uniq" : "idx", tableName, idx.fields);
|
|
452
|
+
if (!existingIndexNames.has(idxName) && !this.failedIndexes.has(idxName)) {
|
|
417
453
|
for (const fName of idx.fields) {
|
|
418
454
|
try {
|
|
419
455
|
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [fName]);
|
|
@@ -431,7 +467,14 @@ FIX INSTRUCTIONS:
|
|
|
431
467
|
existingIndexNames.add(idxName);
|
|
432
468
|
} catch (err) {
|
|
433
469
|
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
434
|
-
|
|
470
|
+
this.failedIndexes.add(idxName);
|
|
471
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
472
|
+
console.warn(
|
|
473
|
+
`[dyrected/mysql] Could not create ${isUnique ? "unique " : ""}index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
474
|
+
);
|
|
475
|
+
} else {
|
|
476
|
+
console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
|
|
477
|
+
}
|
|
435
478
|
}
|
|
436
479
|
}
|
|
437
480
|
}
|
|
@@ -457,7 +500,7 @@ FIX INSTRUCTIONS:
|
|
|
457
500
|
let whereParams = [];
|
|
458
501
|
if (args.where && Object.keys(args.where).length > 0) {
|
|
459
502
|
const result = (0, import_core.parseSqlWhere)(
|
|
460
|
-
args.where,
|
|
503
|
+
this.coerceBooleanWhere(args.collection, args.where),
|
|
461
504
|
(field) => {
|
|
462
505
|
if (field === "id") return "`id`";
|
|
463
506
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -465,7 +508,7 @@ FIX INSTRUCTIONS:
|
|
|
465
508
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
466
509
|
return `\`${field}\``;
|
|
467
510
|
}
|
|
468
|
-
return
|
|
511
|
+
return jsonWhereExpr(field);
|
|
469
512
|
},
|
|
470
513
|
"?"
|
|
471
514
|
);
|
|
@@ -483,12 +526,13 @@ FIX INSTRUCTIONS:
|
|
|
483
526
|
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET ?${lock}`,
|
|
484
527
|
[...whereParams, limit, offset]
|
|
485
528
|
);
|
|
529
|
+
const dateCols = this.dateColumns(args.collection);
|
|
486
530
|
const docs = rows.map((r) => {
|
|
487
531
|
const parsedData = JSON.parse(typeof r.data === "string" ? r.data : JSON.stringify(r.data));
|
|
488
532
|
const promotedOverlay = {};
|
|
489
533
|
for (const col of existingCols) {
|
|
490
534
|
if (!["id", "data", "created_at", "updated_at"].includes(col) && r[col] !== void 0 && r[col] !== null) {
|
|
491
|
-
promotedOverlay[col] = r[col];
|
|
535
|
+
promotedOverlay[col] = dateCols.has(col) ? fromDatetimeColumn(r[col]) : r[col];
|
|
492
536
|
}
|
|
493
537
|
}
|
|
494
538
|
return {
|
|
@@ -511,6 +555,11 @@ FIX INSTRUCTIONS:
|
|
|
511
555
|
};
|
|
512
556
|
}
|
|
513
557
|
async findOne(params) {
|
|
558
|
+
if (params.id === void 0 && params.where) {
|
|
559
|
+
const found = await this.find({ collection: params.collection, where: params.where, limit: 1, lock: params.lock });
|
|
560
|
+
return found.docs[0] ?? null;
|
|
561
|
+
}
|
|
562
|
+
if (params.id === void 0) throw new Error("findOne requires either id or where");
|
|
514
563
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
515
564
|
const tableName = this.getTableName(params.collection);
|
|
516
565
|
const lock = this.inTransaction || params.lock === "for-update" ? " FOR UPDATE" : "";
|
|
@@ -519,10 +568,11 @@ FIX INSTRUCTIONS:
|
|
|
519
568
|
if (!row) return null;
|
|
520
569
|
const parsedData = JSON.parse(typeof row.data === "string" ? row.data : JSON.stringify(row.data));
|
|
521
570
|
const existingCols = await this.getTableColumns(tableName);
|
|
571
|
+
const dateCols = this.dateColumns(params.collection);
|
|
522
572
|
const promotedOverlay = {};
|
|
523
573
|
for (const col of existingCols) {
|
|
524
574
|
if (!["id", "data", "created_at", "updated_at"].includes(col) && row[col] !== void 0 && row[col] !== null) {
|
|
525
|
-
promotedOverlay[col] = row[col];
|
|
575
|
+
promotedOverlay[col] = dateCols.has(col) ? fromDatetimeColumn(row[col]) : row[col];
|
|
526
576
|
}
|
|
527
577
|
}
|
|
528
578
|
return {
|
|
@@ -544,18 +594,19 @@ FIX INSTRUCTIONS:
|
|
|
544
594
|
delete data.id;
|
|
545
595
|
delete data.createdAt;
|
|
546
596
|
delete data.updatedAt;
|
|
597
|
+
const dateCols = this.dateColumns(params.collection);
|
|
547
598
|
const promotedValues = {};
|
|
548
599
|
for (const col of existingCols) {
|
|
549
600
|
if (["id", "data", "created_at", "updated_at"].includes(col)) continue;
|
|
550
601
|
if (data[col] !== void 0) {
|
|
551
|
-
promotedValues[col] = data[col];
|
|
602
|
+
promotedValues[col] = dateCols.has(col) ? toDatetimeColumn(data[col]) : data[col];
|
|
552
603
|
}
|
|
553
604
|
}
|
|
554
605
|
const colNames = ["id", "data", "created_at", "updated_at", ...Object.keys(promotedValues).map((k) => `\`${k}\``)];
|
|
555
606
|
const placeholders = colNames.map(() => "?").join(", ");
|
|
556
607
|
const values = [id, JSON.stringify(data), now, now, ...Object.values(promotedValues)];
|
|
557
608
|
await this.query(`INSERT INTO \`${tableName}\` (${colNames.join(", ")}) VALUES (${placeholders})`, values);
|
|
558
|
-
return { id, ...data, ...promotedValues, createdAt: now, updatedAt: now };
|
|
609
|
+
return { id, ...data, ...promotedValues, ...this.isoDates(dateCols, promotedValues), createdAt: now, updatedAt: now };
|
|
559
610
|
}
|
|
560
611
|
async update(params) {
|
|
561
612
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
@@ -569,7 +620,7 @@ FIX INSTRUCTIONS:
|
|
|
569
620
|
whereParams = [params.id];
|
|
570
621
|
if (params.where && Object.keys(params.where).length > 0) {
|
|
571
622
|
const parsed = (0, import_core.parseSqlWhere)(
|
|
572
|
-
params.where,
|
|
623
|
+
this.coerceBooleanWhere(params.collection, params.where),
|
|
573
624
|
(field) => {
|
|
574
625
|
if (field === "id") return "`id`";
|
|
575
626
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -577,7 +628,7 @@ FIX INSTRUCTIONS:
|
|
|
577
628
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
578
629
|
return `\`${field}\``;
|
|
579
630
|
}
|
|
580
|
-
return
|
|
631
|
+
return jsonWhereExpr(field);
|
|
581
632
|
},
|
|
582
633
|
"?"
|
|
583
634
|
);
|
|
@@ -586,7 +637,7 @@ FIX INSTRUCTIONS:
|
|
|
586
637
|
}
|
|
587
638
|
} else if (params.where && Object.keys(params.where).length > 0) {
|
|
588
639
|
const parsed = (0, import_core.parseSqlWhere)(
|
|
589
|
-
params.where,
|
|
640
|
+
this.coerceBooleanWhere(params.collection, params.where),
|
|
590
641
|
(field) => {
|
|
591
642
|
if (field === "id") return "`id`";
|
|
592
643
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -594,7 +645,7 @@ FIX INSTRUCTIONS:
|
|
|
594
645
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
595
646
|
return `\`${field}\``;
|
|
596
647
|
}
|
|
597
|
-
return
|
|
648
|
+
return jsonWhereExpr(field);
|
|
598
649
|
},
|
|
599
650
|
"?"
|
|
600
651
|
);
|
|
@@ -607,6 +658,7 @@ FIX INSTRUCTIONS:
|
|
|
607
658
|
const setParams = [];
|
|
608
659
|
const jsonPaths = [];
|
|
609
660
|
const jsonParams = [];
|
|
661
|
+
const dateColsForUpdate = this.dateColumns(params.collection);
|
|
610
662
|
for (const [key, val] of Object.entries(params.data)) {
|
|
611
663
|
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
612
664
|
const isPromoted = existingCols.includes(key) && !["id", "data", "created_at", "updated_at"].includes(key);
|
|
@@ -625,7 +677,7 @@ FIX INSTRUCTIONS:
|
|
|
625
677
|
if (isPromoted) {
|
|
626
678
|
const escapedCol = escapeMysqlIdentifier(key);
|
|
627
679
|
setClauses.push(`${escapedCol} = ?`);
|
|
628
|
-
setParams.push(val);
|
|
680
|
+
setParams.push(dateColsForUpdate.has(key) ? toDatetimeColumn(val) : val);
|
|
629
681
|
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
630
682
|
} else if (val === null) {
|
|
631
683
|
jsonPaths.push(`'$.${key}', CAST('null' AS JSON)`);
|
|
@@ -660,6 +712,21 @@ FIX INSTRUCTIONS:
|
|
|
660
712
|
}
|
|
661
713
|
return { id: targetId, ...params.data, affectedRows: res?.affectedRows ?? 0, updatedAt: now };
|
|
662
714
|
}
|
|
715
|
+
/** Re-expresses bound date values as ISO strings for the document returned from a write. */
|
|
716
|
+
isoDates(dateCols, values) {
|
|
717
|
+
const out = {};
|
|
718
|
+
for (const col of dateCols) if (col in values) out[col] = fromDatetimeColumn(values[col]);
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
dateColumns(collection) {
|
|
722
|
+
return new Set(
|
|
723
|
+
(this.collectionConfigs.get(collection)?.fields ?? []).filter((f) => DATE_TYPES.has(f.type) && f.name).map((f) => f.name)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
/** Applies {@link coerceBooleanWhere} using the collection's declared field types. */
|
|
727
|
+
coerceBooleanWhere(collection, where) {
|
|
728
|
+
return (0, import_core.coerceBooleanWhere)(where, this.collectionConfigs.get(collection)?.fields);
|
|
729
|
+
}
|
|
663
730
|
async sync(collections) {
|
|
664
731
|
for (const col of collections) {
|
|
665
732
|
this.collectionConfigs.set(col.slug, col);
|
|
@@ -724,23 +791,30 @@ FIX INSTRUCTIONS:
|
|
|
724
791
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
725
792
|
return `\`${field}\``;
|
|
726
793
|
}
|
|
727
|
-
return
|
|
794
|
+
return jsonWhereExpr(field);
|
|
728
795
|
};
|
|
729
796
|
const toCastExpr = (rawField, cast) => {
|
|
730
797
|
const base = toFieldExpr(rawField);
|
|
731
798
|
if (cast === "string") return base;
|
|
732
799
|
if (cast === "boolean") return `IF(${base} IS NOT NULL, IF(${base} IN ('true','1'), 1, 0), NULL)`;
|
|
733
|
-
if (cast === "date") return `CAST(${base} AS DATETIME)`;
|
|
800
|
+
if (cast === "date") return `IF(${base} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}', CAST(${base} AS DATETIME), NULL)`;
|
|
734
801
|
const sqlType = cast === "integer" ? "SIGNED" : "DECIMAL(20,6)";
|
|
735
802
|
return `IF(${base} REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(${base} AS ${sqlType}), NULL)`;
|
|
736
803
|
};
|
|
804
|
+
const toDistinctValues = (raw) => {
|
|
805
|
+
const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
|
|
806
|
+
if (!Array.isArray(parsed)) return [];
|
|
807
|
+
return Array.from(new Set(parsed.filter((v) => v !== null && v !== void 0)));
|
|
808
|
+
};
|
|
809
|
+
const groupFieldType = args.groupBy ? (this.collectionConfigs.get(args.collection)?.fields ?? []).find((f) => f.name === args.groupBy)?.type : void 0;
|
|
810
|
+
const groupKeyOf = (raw) => (0, import_core.normalizeGroupKey)(raw, groupFieldType);
|
|
737
811
|
const selectParts = [];
|
|
738
812
|
const allParams = [];
|
|
739
813
|
const isDistinctMap = {};
|
|
740
814
|
for (const [name, op] of Object.entries(args.aggregates)) {
|
|
741
815
|
let whereSql = null;
|
|
742
816
|
if (op.where && Object.keys(op.where).length > 0) {
|
|
743
|
-
const parsed = (0, import_core.parseSqlWhere)(op.where, toFieldExpr, "?");
|
|
817
|
+
const parsed = (0, import_core.parseSqlWhere)(this.coerceBooleanWhere(args.collection, op.where), toFieldExpr, "?");
|
|
744
818
|
whereSql = parsed.sql;
|
|
745
819
|
allParams.push(...parsed.params);
|
|
746
820
|
}
|
|
@@ -751,7 +825,7 @@ FIX INSTRUCTIONS:
|
|
|
751
825
|
} else if ("distinct" in op && typeof op.distinct === "string") {
|
|
752
826
|
isDistinctMap[name] = true;
|
|
753
827
|
const fieldExpr = toFieldExpr(op.distinct);
|
|
754
|
-
aggExpr = whereSql ? `COALESCE(JSON_ARRAYAGG(
|
|
828
|
+
aggExpr = whereSql ? `COALESCE(JSON_ARRAYAGG(IF(${whereSql}, ${fieldExpr}, NULL)), JSON_ARRAY())` : `COALESCE(JSON_ARRAYAGG(${fieldExpr}), JSON_ARRAY())`;
|
|
755
829
|
} else if ("count" in op) {
|
|
756
830
|
aggExpr = whereSql ? `COUNT(IF(${whereSql}, 1, NULL))` : `COUNT(*)`;
|
|
757
831
|
} else if (op.sum) {
|
|
@@ -759,7 +833,7 @@ FIX INSTRUCTIONS:
|
|
|
759
833
|
aggExpr = whereSql ? `SUM(IF(${whereSql}, ${val}, NULL))` : `SUM(${val})`;
|
|
760
834
|
} else if (op.avg) {
|
|
761
835
|
const val = toCastExpr(op.avg, op.cast);
|
|
762
|
-
aggExpr = whereSql ? `AVG(IF(${whereSql}, ${val}, NULL))` : `AVG(${val})`;
|
|
836
|
+
aggExpr = whereSql ? `CAST(AVG(IF(${whereSql}, ${val}, NULL)) AS DOUBLE)` : `CAST(AVG(${val}) AS DOUBLE)`;
|
|
763
837
|
} else if (op.min) {
|
|
764
838
|
const val = toCastExpr(op.min, op.cast);
|
|
765
839
|
aggExpr = whereSql ? `MIN(IF(${whereSql}, ${val}, NULL))` : `MIN(${val})`;
|
|
@@ -779,13 +853,12 @@ FIX INSTRUCTIONS:
|
|
|
779
853
|
const [rows2] = await this.query(query2, allParams);
|
|
780
854
|
const groups = {};
|
|
781
855
|
for (const row2 of rows2 ?? []) {
|
|
782
|
-
const key =
|
|
856
|
+
const key = groupKeyOf(row2.__group_key);
|
|
783
857
|
const groupResult = {};
|
|
784
858
|
for (const name of Object.keys(args.aggregates)) {
|
|
785
859
|
const raw = row2[name];
|
|
786
860
|
if (isDistinctMap[name]) {
|
|
787
|
-
|
|
788
|
-
groupResult[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
|
|
861
|
+
groupResult[name] = toDistinctValues(raw);
|
|
789
862
|
} else {
|
|
790
863
|
groupResult[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
791
864
|
}
|
|
@@ -800,11 +873,15 @@ FIX INSTRUCTIONS:
|
|
|
800
873
|
const result = {};
|
|
801
874
|
for (const name of Object.keys(args.aggregates)) {
|
|
802
875
|
const raw = row[name];
|
|
876
|
+
const op = args.aggregates[name];
|
|
803
877
|
if (isDistinctMap[name]) {
|
|
804
|
-
|
|
805
|
-
|
|
878
|
+
result[name] = toDistinctValues(raw);
|
|
879
|
+
} else if (raw instanceof Date) {
|
|
880
|
+
result[name] = raw.toISOString();
|
|
881
|
+
} else if (op?.cast === "date") {
|
|
882
|
+
result[name] = raw === null || raw === void 0 ? null : String(raw);
|
|
806
883
|
} else {
|
|
807
|
-
result[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
884
|
+
result[name] = raw === null || raw === void 0 ? null : isNaN(Number(raw)) ? raw : Number(raw);
|
|
808
885
|
}
|
|
809
886
|
}
|
|
810
887
|
return result;
|
|
@@ -814,6 +891,7 @@ FIX INSTRUCTIONS:
|
|
|
814
891
|
const cacheKey = getMysqlCacheKey(this.config);
|
|
815
892
|
cache.delete(cacheKey);
|
|
816
893
|
this.ensuredTables.clear();
|
|
894
|
+
this.failedIndexes.clear();
|
|
817
895
|
this.tableColumnsCache.clear();
|
|
818
896
|
if (this.initPromise) {
|
|
819
897
|
try {
|
package/dist/index.d.cts
CHANGED
|
@@ -19,6 +19,7 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
19
19
|
private inTransaction;
|
|
20
20
|
private tableLocks;
|
|
21
21
|
private ensuredTables;
|
|
22
|
+
private failedIndexes;
|
|
22
23
|
private tableColumnsCache;
|
|
23
24
|
private collectionConfigs;
|
|
24
25
|
constructor(config: MysqlAdapterConfig);
|
|
@@ -43,7 +44,8 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
43
44
|
}): Promise<PaginatedResult>;
|
|
44
45
|
findOne(params: {
|
|
45
46
|
collection: string;
|
|
46
|
-
id
|
|
47
|
+
id?: string;
|
|
48
|
+
where?: Record<string, unknown>;
|
|
47
49
|
lock?: "for-update";
|
|
48
50
|
}): Promise<any>;
|
|
49
51
|
create(params: {
|
|
@@ -56,6 +58,11 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
56
58
|
where?: any;
|
|
57
59
|
data: any;
|
|
58
60
|
}): Promise<any>;
|
|
61
|
+
/** Re-expresses bound date values as ISO strings for the document returned from a write. */
|
|
62
|
+
private isoDates;
|
|
63
|
+
private dateColumns;
|
|
64
|
+
/** Applies {@link coerceBooleanWhere} using the collection's declared field types. */
|
|
65
|
+
private coerceBooleanWhere;
|
|
59
66
|
sync(collections: any[]): Promise<void>;
|
|
60
67
|
delete(params: {
|
|
61
68
|
collection: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
19
19
|
private inTransaction;
|
|
20
20
|
private tableLocks;
|
|
21
21
|
private ensuredTables;
|
|
22
|
+
private failedIndexes;
|
|
22
23
|
private tableColumnsCache;
|
|
23
24
|
private collectionConfigs;
|
|
24
25
|
constructor(config: MysqlAdapterConfig);
|
|
@@ -43,7 +44,8 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
43
44
|
}): Promise<PaginatedResult>;
|
|
44
45
|
findOne(params: {
|
|
45
46
|
collection: string;
|
|
46
|
-
id
|
|
47
|
+
id?: string;
|
|
48
|
+
where?: Record<string, unknown>;
|
|
47
49
|
lock?: "for-update";
|
|
48
50
|
}): Promise<any>;
|
|
49
51
|
create(params: {
|
|
@@ -56,6 +58,11 @@ declare class MysqlAdapter implements DatabaseAdapter {
|
|
|
56
58
|
where?: any;
|
|
57
59
|
data: any;
|
|
58
60
|
}): Promise<any>;
|
|
61
|
+
/** Re-expresses bound date values as ISO strings for the document returned from a write. */
|
|
62
|
+
private isoDates;
|
|
63
|
+
private dateColumns;
|
|
64
|
+
/** Applies {@link coerceBooleanWhere} using the collection's declared field types. */
|
|
65
|
+
private coerceBooleanWhere;
|
|
59
66
|
sync(collections: any[]): Promise<void>;
|
|
60
67
|
delete(params: {
|
|
61
68
|
collection: string;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { parseSort, parseSqlWhere, DuplicateKeyError, generateDocumentId } from "@dyrected/core";
|
|
2
|
+
import { parseSort, parseSqlWhere, DuplicateKeyError, generateDocumentId, resolveIndexName, coerceBooleanWhere, normalizeGroupKey } from "@dyrected/core";
|
|
3
3
|
import mysql from "mysql2/promise";
|
|
4
4
|
function parseMysqlDuplicateKey(message) {
|
|
5
5
|
const match = message.match(/Duplicate entry '(.*)' for key '(?:[^.]*\.)?(.*)'/);
|
|
@@ -14,6 +14,24 @@ function parseMysqlDuplicateKey(message) {
|
|
|
14
14
|
}
|
|
15
15
|
return { field, value };
|
|
16
16
|
}
|
|
17
|
+
function jsonWhereExpr(field) {
|
|
18
|
+
const path = `JSON_EXTRACT(data, '$.${field}')`;
|
|
19
|
+
return `IF(JSON_TYPE(${path}) = 'NULL', NULL, IF(JSON_TYPE(${path}) = 'BOOLEAN', IF(${path} = TRUE, 1, 0), JSON_UNQUOTE(${path})))`;
|
|
20
|
+
}
|
|
21
|
+
var DATE_TYPES = /* @__PURE__ */ new Set(["date", "datetime"]);
|
|
22
|
+
var ISO_WITH_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
23
|
+
var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
24
|
+
function toDatetimeColumn(value) {
|
|
25
|
+
if (typeof value !== "string" || !ISO_WITH_TIME.test(value)) return value;
|
|
26
|
+
const date = new Date(value);
|
|
27
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
28
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
29
|
+
}
|
|
30
|
+
function fromDatetimeColumn(value) {
|
|
31
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
32
|
+
if (typeof value === "string" && MYSQL_DATETIME.test(value)) return `${value.replace(" ", "T")}Z`;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
17
35
|
function isNumericOp(val) {
|
|
18
36
|
return val !== null && typeof val === "object" && !Array.isArray(val) && ("increment" in val || "decrement" in val);
|
|
19
37
|
}
|
|
@@ -49,6 +67,7 @@ var MysqlAdapter = class {
|
|
|
49
67
|
inTransaction = false;
|
|
50
68
|
tableLocks = /* @__PURE__ */ new Map();
|
|
51
69
|
ensuredTables = /* @__PURE__ */ new Set();
|
|
70
|
+
failedIndexes = /* @__PURE__ */ new Set();
|
|
52
71
|
tableColumnsCache = /* @__PURE__ */ new Map();
|
|
53
72
|
collectionConfigs = /* @__PURE__ */ new Map();
|
|
54
73
|
constructor(config) {
|
|
@@ -315,7 +334,15 @@ FIX INSTRUCTIONS:
|
|
|
315
334
|
console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
|
|
316
335
|
const isUniqueOrIndexed = field.unique || indexedFields.has(field.name);
|
|
317
336
|
let sqlType = isUniqueOrIndexed ? "VARCHAR(191)" : "TEXT";
|
|
337
|
+
if (!isUniqueOrIndexed) {
|
|
338
|
+
if ((field.type === "select" || field.type === "radio") && !field.hasMany) sqlType = "VARCHAR(100)";
|
|
339
|
+
else if (field.type === "relationship" && !field.hasMany) sqlType = "VARCHAR(191)";
|
|
340
|
+
else if (field.type === "email") sqlType = "VARCHAR(254)";
|
|
341
|
+
} else if (field.type === "email") {
|
|
342
|
+
sqlType = "VARCHAR(191)";
|
|
343
|
+
}
|
|
318
344
|
if (field.type === "number") sqlType = "DECIMAL(19,4)";
|
|
345
|
+
if (field.type === "money") sqlType = "BIGINT";
|
|
319
346
|
if (field.type === "boolean") sqlType = "TINYINT(1)";
|
|
320
347
|
if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
|
|
321
348
|
if (field.type === "textarea" || field.type === "richText" || field.type === "json") sqlType = "LONGTEXT";
|
|
@@ -333,6 +360,8 @@ FIX INSTRUCTIONS:
|
|
|
333
360
|
let castExpr = `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}'))`;
|
|
334
361
|
if (field.type === "number") {
|
|
335
362
|
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) AS DECIMAL(19,4)), NULL)`;
|
|
363
|
+
} else if (field.type === "money") {
|
|
364
|
+
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) REGEXP '^-?[0-9]+$', CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) AS SIGNED), NULL)`;
|
|
336
365
|
} else if (field.type === "boolean") {
|
|
337
366
|
castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) IN ('true', '1'), 1, 0)`;
|
|
338
367
|
} else if (field.type === "date" || field.type === "datetime") {
|
|
@@ -351,8 +380,8 @@ FIX INSTRUCTIONS:
|
|
|
351
380
|
const existingIndexNames = new Set(existingIndicesRows.map((r) => r?.Key_name).filter(Boolean));
|
|
352
381
|
for (const field of fields) {
|
|
353
382
|
if (field.unique) {
|
|
354
|
-
const idxName =
|
|
355
|
-
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name)) {
|
|
383
|
+
const idxName = resolveIndexName("uniq", tableName, [field.name]);
|
|
384
|
+
if (!existingIndexNames.has(idxName) && !existingIndexNames.has(field.name) && !this.failedIndexes.has(idxName)) {
|
|
356
385
|
try {
|
|
357
386
|
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [field.name]);
|
|
358
387
|
const colType = colInfoRows?.[0]?.Type?.toLowerCase() || "";
|
|
@@ -366,7 +395,14 @@ FIX INSTRUCTIONS:
|
|
|
366
395
|
existingIndexNames.add(idxName);
|
|
367
396
|
} catch (err) {
|
|
368
397
|
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
369
|
-
|
|
398
|
+
this.failedIndexes.add(idxName);
|
|
399
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
400
|
+
console.warn(
|
|
401
|
+
`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
|
|
405
|
+
}
|
|
370
406
|
}
|
|
371
407
|
}
|
|
372
408
|
}
|
|
@@ -376,8 +412,8 @@ FIX INSTRUCTIONS:
|
|
|
376
412
|
for (const idx of indexes) {
|
|
377
413
|
if (!Array.isArray(idx.fields) || idx.fields.length === 0) continue;
|
|
378
414
|
const isUnique = Boolean(idx.unique);
|
|
379
|
-
const idxName = idx.name ||
|
|
380
|
-
if (!existingIndexNames.has(idxName)) {
|
|
415
|
+
const idxName = idx.name || resolveIndexName(isUnique ? "uniq" : "idx", tableName, idx.fields);
|
|
416
|
+
if (!existingIndexNames.has(idxName) && !this.failedIndexes.has(idxName)) {
|
|
381
417
|
for (const fName of idx.fields) {
|
|
382
418
|
try {
|
|
383
419
|
const [colInfoRows] = await this.query(`SHOW COLUMNS FROM \`${tableName}\` WHERE Field = ?`, [fName]);
|
|
@@ -395,7 +431,14 @@ FIX INSTRUCTIONS:
|
|
|
395
431
|
existingIndexNames.add(idxName);
|
|
396
432
|
} catch (err) {
|
|
397
433
|
if (err.errno !== 1061 && err.code !== "ER_DUP_KEYNAME") {
|
|
398
|
-
|
|
434
|
+
this.failedIndexes.add(idxName);
|
|
435
|
+
if (err.errno === 1062 || err.code === "ER_DUP_ENTRY") {
|
|
436
|
+
console.warn(
|
|
437
|
+
`[dyrected/mysql] Could not create ${isUnique ? "unique " : ""}index ${idxName} on ${tableName} because duplicate values already exist: ${err.message}. To enforce uniqueness, remove duplicate records.`
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
|
|
441
|
+
}
|
|
399
442
|
}
|
|
400
443
|
}
|
|
401
444
|
}
|
|
@@ -421,7 +464,7 @@ FIX INSTRUCTIONS:
|
|
|
421
464
|
let whereParams = [];
|
|
422
465
|
if (args.where && Object.keys(args.where).length > 0) {
|
|
423
466
|
const result = parseSqlWhere(
|
|
424
|
-
args.where,
|
|
467
|
+
this.coerceBooleanWhere(args.collection, args.where),
|
|
425
468
|
(field) => {
|
|
426
469
|
if (field === "id") return "`id`";
|
|
427
470
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -429,7 +472,7 @@ FIX INSTRUCTIONS:
|
|
|
429
472
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
430
473
|
return `\`${field}\``;
|
|
431
474
|
}
|
|
432
|
-
return
|
|
475
|
+
return jsonWhereExpr(field);
|
|
433
476
|
},
|
|
434
477
|
"?"
|
|
435
478
|
);
|
|
@@ -447,12 +490,13 @@ FIX INSTRUCTIONS:
|
|
|
447
490
|
`SELECT * FROM \`${tableName}\` ${whereSql} ORDER BY ${sort} LIMIT ? OFFSET ?${lock}`,
|
|
448
491
|
[...whereParams, limit, offset]
|
|
449
492
|
);
|
|
493
|
+
const dateCols = this.dateColumns(args.collection);
|
|
450
494
|
const docs = rows.map((r) => {
|
|
451
495
|
const parsedData = JSON.parse(typeof r.data === "string" ? r.data : JSON.stringify(r.data));
|
|
452
496
|
const promotedOverlay = {};
|
|
453
497
|
for (const col of existingCols) {
|
|
454
498
|
if (!["id", "data", "created_at", "updated_at"].includes(col) && r[col] !== void 0 && r[col] !== null) {
|
|
455
|
-
promotedOverlay[col] = r[col];
|
|
499
|
+
promotedOverlay[col] = dateCols.has(col) ? fromDatetimeColumn(r[col]) : r[col];
|
|
456
500
|
}
|
|
457
501
|
}
|
|
458
502
|
return {
|
|
@@ -475,6 +519,11 @@ FIX INSTRUCTIONS:
|
|
|
475
519
|
};
|
|
476
520
|
}
|
|
477
521
|
async findOne(params) {
|
|
522
|
+
if (params.id === void 0 && params.where) {
|
|
523
|
+
const found = await this.find({ collection: params.collection, where: params.where, limit: 1, lock: params.lock });
|
|
524
|
+
return found.docs[0] ?? null;
|
|
525
|
+
}
|
|
526
|
+
if (params.id === void 0) throw new Error("findOne requires either id or where");
|
|
478
527
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
479
528
|
const tableName = this.getTableName(params.collection);
|
|
480
529
|
const lock = this.inTransaction || params.lock === "for-update" ? " FOR UPDATE" : "";
|
|
@@ -483,10 +532,11 @@ FIX INSTRUCTIONS:
|
|
|
483
532
|
if (!row) return null;
|
|
484
533
|
const parsedData = JSON.parse(typeof row.data === "string" ? row.data : JSON.stringify(row.data));
|
|
485
534
|
const existingCols = await this.getTableColumns(tableName);
|
|
535
|
+
const dateCols = this.dateColumns(params.collection);
|
|
486
536
|
const promotedOverlay = {};
|
|
487
537
|
for (const col of existingCols) {
|
|
488
538
|
if (!["id", "data", "created_at", "updated_at"].includes(col) && row[col] !== void 0 && row[col] !== null) {
|
|
489
|
-
promotedOverlay[col] = row[col];
|
|
539
|
+
promotedOverlay[col] = dateCols.has(col) ? fromDatetimeColumn(row[col]) : row[col];
|
|
490
540
|
}
|
|
491
541
|
}
|
|
492
542
|
return {
|
|
@@ -508,18 +558,19 @@ FIX INSTRUCTIONS:
|
|
|
508
558
|
delete data.id;
|
|
509
559
|
delete data.createdAt;
|
|
510
560
|
delete data.updatedAt;
|
|
561
|
+
const dateCols = this.dateColumns(params.collection);
|
|
511
562
|
const promotedValues = {};
|
|
512
563
|
for (const col of existingCols) {
|
|
513
564
|
if (["id", "data", "created_at", "updated_at"].includes(col)) continue;
|
|
514
565
|
if (data[col] !== void 0) {
|
|
515
|
-
promotedValues[col] = data[col];
|
|
566
|
+
promotedValues[col] = dateCols.has(col) ? toDatetimeColumn(data[col]) : data[col];
|
|
516
567
|
}
|
|
517
568
|
}
|
|
518
569
|
const colNames = ["id", "data", "created_at", "updated_at", ...Object.keys(promotedValues).map((k) => `\`${k}\``)];
|
|
519
570
|
const placeholders = colNames.map(() => "?").join(", ");
|
|
520
571
|
const values = [id, JSON.stringify(data), now, now, ...Object.values(promotedValues)];
|
|
521
572
|
await this.query(`INSERT INTO \`${tableName}\` (${colNames.join(", ")}) VALUES (${placeholders})`, values);
|
|
522
|
-
return { id, ...data, ...promotedValues, createdAt: now, updatedAt: now };
|
|
573
|
+
return { id, ...data, ...promotedValues, ...this.isoDates(dateCols, promotedValues), createdAt: now, updatedAt: now };
|
|
523
574
|
}
|
|
524
575
|
async update(params) {
|
|
525
576
|
if (!this.inTransaction) await this.ensureTable(params.collection);
|
|
@@ -533,7 +584,7 @@ FIX INSTRUCTIONS:
|
|
|
533
584
|
whereParams = [params.id];
|
|
534
585
|
if (params.where && Object.keys(params.where).length > 0) {
|
|
535
586
|
const parsed = parseSqlWhere(
|
|
536
|
-
params.where,
|
|
587
|
+
this.coerceBooleanWhere(params.collection, params.where),
|
|
537
588
|
(field) => {
|
|
538
589
|
if (field === "id") return "`id`";
|
|
539
590
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -541,7 +592,7 @@ FIX INSTRUCTIONS:
|
|
|
541
592
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
542
593
|
return `\`${field}\``;
|
|
543
594
|
}
|
|
544
|
-
return
|
|
595
|
+
return jsonWhereExpr(field);
|
|
545
596
|
},
|
|
546
597
|
"?"
|
|
547
598
|
);
|
|
@@ -550,7 +601,7 @@ FIX INSTRUCTIONS:
|
|
|
550
601
|
}
|
|
551
602
|
} else if (params.where && Object.keys(params.where).length > 0) {
|
|
552
603
|
const parsed = parseSqlWhere(
|
|
553
|
-
params.where,
|
|
604
|
+
this.coerceBooleanWhere(params.collection, params.where),
|
|
554
605
|
(field) => {
|
|
555
606
|
if (field === "id") return "`id`";
|
|
556
607
|
if (field === "createdAt" || field === "created_at") return "`created_at`";
|
|
@@ -558,7 +609,7 @@ FIX INSTRUCTIONS:
|
|
|
558
609
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
559
610
|
return `\`${field}\``;
|
|
560
611
|
}
|
|
561
|
-
return
|
|
612
|
+
return jsonWhereExpr(field);
|
|
562
613
|
},
|
|
563
614
|
"?"
|
|
564
615
|
);
|
|
@@ -571,6 +622,7 @@ FIX INSTRUCTIONS:
|
|
|
571
622
|
const setParams = [];
|
|
572
623
|
const jsonPaths = [];
|
|
573
624
|
const jsonParams = [];
|
|
625
|
+
const dateColsForUpdate = this.dateColumns(params.collection);
|
|
574
626
|
for (const [key, val] of Object.entries(params.data)) {
|
|
575
627
|
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
576
628
|
const isPromoted = existingCols.includes(key) && !["id", "data", "created_at", "updated_at"].includes(key);
|
|
@@ -589,7 +641,7 @@ FIX INSTRUCTIONS:
|
|
|
589
641
|
if (isPromoted) {
|
|
590
642
|
const escapedCol = escapeMysqlIdentifier(key);
|
|
591
643
|
setClauses.push(`${escapedCol} = ?`);
|
|
592
|
-
setParams.push(val);
|
|
644
|
+
setParams.push(dateColsForUpdate.has(key) ? toDatetimeColumn(val) : val);
|
|
593
645
|
jsonPaths.push(`'$.${key}', ${escapedCol}`);
|
|
594
646
|
} else if (val === null) {
|
|
595
647
|
jsonPaths.push(`'$.${key}', CAST('null' AS JSON)`);
|
|
@@ -624,6 +676,21 @@ FIX INSTRUCTIONS:
|
|
|
624
676
|
}
|
|
625
677
|
return { id: targetId, ...params.data, affectedRows: res?.affectedRows ?? 0, updatedAt: now };
|
|
626
678
|
}
|
|
679
|
+
/** Re-expresses bound date values as ISO strings for the document returned from a write. */
|
|
680
|
+
isoDates(dateCols, values) {
|
|
681
|
+
const out = {};
|
|
682
|
+
for (const col of dateCols) if (col in values) out[col] = fromDatetimeColumn(values[col]);
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
dateColumns(collection) {
|
|
686
|
+
return new Set(
|
|
687
|
+
(this.collectionConfigs.get(collection)?.fields ?? []).filter((f) => DATE_TYPES.has(f.type) && f.name).map((f) => f.name)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
/** Applies {@link coerceBooleanWhere} using the collection's declared field types. */
|
|
691
|
+
coerceBooleanWhere(collection, where) {
|
|
692
|
+
return coerceBooleanWhere(where, this.collectionConfigs.get(collection)?.fields);
|
|
693
|
+
}
|
|
627
694
|
async sync(collections) {
|
|
628
695
|
for (const col of collections) {
|
|
629
696
|
this.collectionConfigs.set(col.slug, col);
|
|
@@ -688,23 +755,30 @@ FIX INSTRUCTIONS:
|
|
|
688
755
|
if (existingCols.includes(field) && !["id", "data"].includes(field)) {
|
|
689
756
|
return `\`${field}\``;
|
|
690
757
|
}
|
|
691
|
-
return
|
|
758
|
+
return jsonWhereExpr(field);
|
|
692
759
|
};
|
|
693
760
|
const toCastExpr = (rawField, cast) => {
|
|
694
761
|
const base = toFieldExpr(rawField);
|
|
695
762
|
if (cast === "string") return base;
|
|
696
763
|
if (cast === "boolean") return `IF(${base} IS NOT NULL, IF(${base} IN ('true','1'), 1, 0), NULL)`;
|
|
697
|
-
if (cast === "date") return `CAST(${base} AS DATETIME)`;
|
|
764
|
+
if (cast === "date") return `IF(${base} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}', CAST(${base} AS DATETIME), NULL)`;
|
|
698
765
|
const sqlType = cast === "integer" ? "SIGNED" : "DECIMAL(20,6)";
|
|
699
766
|
return `IF(${base} REGEXP '^-?[0-9]+(\\\\.[0-9]+)?([eE][+-]?[0-9]+)?$', CAST(${base} AS ${sqlType}), NULL)`;
|
|
700
767
|
};
|
|
768
|
+
const toDistinctValues = (raw) => {
|
|
769
|
+
const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
|
|
770
|
+
if (!Array.isArray(parsed)) return [];
|
|
771
|
+
return Array.from(new Set(parsed.filter((v) => v !== null && v !== void 0)));
|
|
772
|
+
};
|
|
773
|
+
const groupFieldType = args.groupBy ? (this.collectionConfigs.get(args.collection)?.fields ?? []).find((f) => f.name === args.groupBy)?.type : void 0;
|
|
774
|
+
const groupKeyOf = (raw) => normalizeGroupKey(raw, groupFieldType);
|
|
701
775
|
const selectParts = [];
|
|
702
776
|
const allParams = [];
|
|
703
777
|
const isDistinctMap = {};
|
|
704
778
|
for (const [name, op] of Object.entries(args.aggregates)) {
|
|
705
779
|
let whereSql = null;
|
|
706
780
|
if (op.where && Object.keys(op.where).length > 0) {
|
|
707
|
-
const parsed = parseSqlWhere(op.where, toFieldExpr, "?");
|
|
781
|
+
const parsed = parseSqlWhere(this.coerceBooleanWhere(args.collection, op.where), toFieldExpr, "?");
|
|
708
782
|
whereSql = parsed.sql;
|
|
709
783
|
allParams.push(...parsed.params);
|
|
710
784
|
}
|
|
@@ -715,7 +789,7 @@ FIX INSTRUCTIONS:
|
|
|
715
789
|
} else if ("distinct" in op && typeof op.distinct === "string") {
|
|
716
790
|
isDistinctMap[name] = true;
|
|
717
791
|
const fieldExpr = toFieldExpr(op.distinct);
|
|
718
|
-
aggExpr = whereSql ? `COALESCE(JSON_ARRAYAGG(
|
|
792
|
+
aggExpr = whereSql ? `COALESCE(JSON_ARRAYAGG(IF(${whereSql}, ${fieldExpr}, NULL)), JSON_ARRAY())` : `COALESCE(JSON_ARRAYAGG(${fieldExpr}), JSON_ARRAY())`;
|
|
719
793
|
} else if ("count" in op) {
|
|
720
794
|
aggExpr = whereSql ? `COUNT(IF(${whereSql}, 1, NULL))` : `COUNT(*)`;
|
|
721
795
|
} else if (op.sum) {
|
|
@@ -723,7 +797,7 @@ FIX INSTRUCTIONS:
|
|
|
723
797
|
aggExpr = whereSql ? `SUM(IF(${whereSql}, ${val}, NULL))` : `SUM(${val})`;
|
|
724
798
|
} else if (op.avg) {
|
|
725
799
|
const val = toCastExpr(op.avg, op.cast);
|
|
726
|
-
aggExpr = whereSql ? `AVG(IF(${whereSql}, ${val}, NULL))` : `AVG(${val})`;
|
|
800
|
+
aggExpr = whereSql ? `CAST(AVG(IF(${whereSql}, ${val}, NULL)) AS DOUBLE)` : `CAST(AVG(${val}) AS DOUBLE)`;
|
|
727
801
|
} else if (op.min) {
|
|
728
802
|
const val = toCastExpr(op.min, op.cast);
|
|
729
803
|
aggExpr = whereSql ? `MIN(IF(${whereSql}, ${val}, NULL))` : `MIN(${val})`;
|
|
@@ -743,13 +817,12 @@ FIX INSTRUCTIONS:
|
|
|
743
817
|
const [rows2] = await this.query(query2, allParams);
|
|
744
818
|
const groups = {};
|
|
745
819
|
for (const row2 of rows2 ?? []) {
|
|
746
|
-
const key =
|
|
820
|
+
const key = groupKeyOf(row2.__group_key);
|
|
747
821
|
const groupResult = {};
|
|
748
822
|
for (const name of Object.keys(args.aggregates)) {
|
|
749
823
|
const raw = row2[name];
|
|
750
824
|
if (isDistinctMap[name]) {
|
|
751
|
-
|
|
752
|
-
groupResult[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
|
|
825
|
+
groupResult[name] = toDistinctValues(raw);
|
|
753
826
|
} else {
|
|
754
827
|
groupResult[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
755
828
|
}
|
|
@@ -764,11 +837,15 @@ FIX INSTRUCTIONS:
|
|
|
764
837
|
const result = {};
|
|
765
838
|
for (const name of Object.keys(args.aggregates)) {
|
|
766
839
|
const raw = row[name];
|
|
840
|
+
const op = args.aggregates[name];
|
|
767
841
|
if (isDistinctMap[name]) {
|
|
768
|
-
|
|
769
|
-
|
|
842
|
+
result[name] = toDistinctValues(raw);
|
|
843
|
+
} else if (raw instanceof Date) {
|
|
844
|
+
result[name] = raw.toISOString();
|
|
845
|
+
} else if (op?.cast === "date") {
|
|
846
|
+
result[name] = raw === null || raw === void 0 ? null : String(raw);
|
|
770
847
|
} else {
|
|
771
|
-
result[name] = raw === null || raw === void 0 ? null : Number(raw);
|
|
848
|
+
result[name] = raw === null || raw === void 0 ? null : isNaN(Number(raw)) ? raw : Number(raw);
|
|
772
849
|
}
|
|
773
850
|
}
|
|
774
851
|
return result;
|
|
@@ -778,6 +855,7 @@ FIX INSTRUCTIONS:
|
|
|
778
855
|
const cacheKey = getMysqlCacheKey(this.config);
|
|
779
856
|
cache.delete(cacheKey);
|
|
780
857
|
this.ensuredTables.clear();
|
|
858
|
+
this.failedIndexes.clear();
|
|
781
859
|
this.tableColumnsCache.clear();
|
|
782
860
|
if (this.initPromise) {
|
|
783
861
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dyrected/db-mysql",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.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.18.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"tsup": "^8.0.0",
|