@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 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 = `uniq_${tableName}_${field.name}`;
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
- console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
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 || `${isUnique ? "uniq" : "idx"}_${tableName}_${idx.fields.join("_")}`;
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
- console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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(DISTINCT IF(${whereSql}, ${fieldExpr}, NULL)), JSON_ARRAY())` : `COALESCE(JSON_ARRAYAGG(DISTINCT ${fieldExpr}), JSON_ARRAY())`;
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 = row2.__group_key === null || row2.__group_key === void 0 ? "__unassigned__" : String(row2.__group_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
- const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
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
- const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
805
- result[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
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: string;
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: string;
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 = `uniq_${tableName}_${field.name}`;
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
- console.warn(`[dyrected/mysql] Could not create unique index ${idxName} on ${tableName}:`, err.message);
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 || `${isUnique ? "uniq" : "idx"}_${tableName}_${idx.fields.join("_")}`;
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
- console.warn(`[dyrected/mysql] Could not create index ${idxName} on ${tableName}:`, err.message);
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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 `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field}'))`;
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(DISTINCT IF(${whereSql}, ${fieldExpr}, NULL)), JSON_ARRAY())` : `COALESCE(JSON_ARRAYAGG(DISTINCT ${fieldExpr}), JSON_ARRAY())`;
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 = row2.__group_key === null || row2.__group_key === void 0 ? "__unassigned__" : String(row2.__group_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
- const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
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
- const parsed = Array.isArray(raw) ? raw : typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
769
- result[name] = Array.isArray(parsed) ? parsed.filter((v) => v !== null && v !== void 0) : [];
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.16.0",
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.16.0"
21
+ "@dyrected/core": "^2.18.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "tsup": "^8.0.0",