@dyrected/db-mysql 2.12.0 → 2.13.1

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
@@ -31,11 +31,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  MysqlAdapter: () => MysqlAdapter,
34
+ closeAllMysqlClients: () => closeAllMysqlClients,
34
35
  mysqlAdapter: () => mysqlAdapter
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
  var import_core = require("@dyrected/core");
38
39
  var import_promise = __toESM(require("mysql2/promise"), 1);
40
+ var MYSQL_CLIENT_CACHE_KEY = "__dyrectedMysqlClientCache";
41
+ function getSharedMysqlClientCache() {
42
+ const globalScope = globalThis;
43
+ if (!globalScope[MYSQL_CLIENT_CACHE_KEY]) {
44
+ globalScope[MYSQL_CLIENT_CACHE_KEY] = /* @__PURE__ */ new Map();
45
+ }
46
+ return globalScope[MYSQL_CLIENT_CACHE_KEY];
47
+ }
48
+ function getMysqlCacheKey(config) {
49
+ if (config.url) return config.url;
50
+ return `${config.host || "localhost"}:${config.port || 3306}:${config.database || ""}:${config.user || ""}`;
51
+ }
39
52
  function escapeMysqlIdentifier(identifier) {
40
53
  return `\`${identifier.replace(/`/g, "``")}\``;
41
54
  }
@@ -54,6 +67,9 @@ var MysqlAdapter = class {
54
67
  config;
55
68
  initPromise = null;
56
69
  inTransaction = false;
70
+ tableLocks = /* @__PURE__ */ new Map();
71
+ ensuredTables = /* @__PURE__ */ new Set();
72
+ tableColumnsCache = /* @__PURE__ */ new Map();
57
73
  constructor(config) {
58
74
  this.config = config;
59
75
  }
@@ -76,6 +92,25 @@ FIX INSTRUCTIONS:
76
92
  }
77
93
  throw err;
78
94
  }
95
+ async retryOperation(operation, maxRetries = 3, initialDelay = 1e3) {
96
+ let lastError;
97
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
98
+ try {
99
+ return await operation();
100
+ } catch (err) {
101
+ lastError = err;
102
+ const isNetworkOrTimeout = err.code === "ETIMEDOUT" || err.code === "ECONNREFUSED" || err.code === "PROTOCOL_CONNECTION_LOST" || err.message?.includes("ETIMEDOUT") || err.message?.includes("Connection lost");
103
+ if (attempt < maxRetries && isNetworkOrTimeout) {
104
+ const delay = initialDelay * Math.pow(2, attempt - 1);
105
+ console.warn(`[dyrected/db-mysql] Connection attempt ${attempt} failed (${err.code || err.message}), retrying in ${delay}ms...`);
106
+ await new Promise((resolve) => setTimeout(resolve, delay));
107
+ } else {
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ throw lastError;
113
+ }
79
114
  async ensureInitialized() {
80
115
  if (!this.initPromise) {
81
116
  this.initPromise = this.initialize();
@@ -83,59 +118,126 @@ FIX INSTRUCTIONS:
83
118
  await this.initPromise;
84
119
  }
85
120
  async initialize() {
86
- const config = this.config;
87
- let dbName = config.database;
88
- let serverConfig = null;
89
- if (config.url) {
90
- try {
91
- const parsed = new URL(config.url);
92
- dbName = parsed.pathname.replace(/^\//, "");
93
- const serverUrl = `${parsed.protocol}//${parsed.username}:${parsed.password}@${parsed.host}`;
94
- serverConfig = serverUrl;
95
- } catch (err) {
121
+ const cache = getSharedMysqlClientCache();
122
+ const cacheKey = getMysqlCacheKey(this.config);
123
+ const cached = cache.get(cacheKey);
124
+ if (cached?.pool) {
125
+ this.pool = cached.pool;
126
+ return;
127
+ }
128
+ if (cached?.initPromise) {
129
+ this.pool = await cached.initPromise;
130
+ return;
131
+ }
132
+ const initPromise = (async () => {
133
+ const config = this.config;
134
+ let dbName = config.database;
135
+ let serverConfig = null;
136
+ if (config.url) {
137
+ try {
138
+ const parsed = new URL(config.url);
139
+ dbName = parsed.pathname.replace(/^\//, "");
140
+ const serverUrl = `${parsed.protocol}//${parsed.username}:${parsed.password}@${parsed.host}`;
141
+ serverConfig = serverUrl;
142
+ } catch (err) {
143
+ }
144
+ } else {
145
+ serverConfig = {
146
+ host: config.host ?? "localhost",
147
+ port: config.port ?? 3306,
148
+ user: config.user,
149
+ password: config.password
150
+ };
96
151
  }
97
- } else {
98
- serverConfig = {
99
- host: config.host ?? "localhost",
100
- port: config.port ?? 3306,
101
- user: config.user,
102
- password: config.password
152
+ if (dbName && serverConfig) {
153
+ try {
154
+ await this.retryOperation(async () => {
155
+ const tempConn = await import_promise.default.createConnection(serverConfig);
156
+ try {
157
+ await tempConn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
158
+ } finally {
159
+ await tempConn.end().catch(() => {
160
+ });
161
+ }
162
+ }, 3, 1e3);
163
+ console.log(`[dyrected/db-mysql] Database "${dbName}" checked/created successfully`);
164
+ } catch (err) {
165
+ console.warn(`[dyrected/db-mysql] Auto-creation of database "${dbName}" skipped/failed:`, err.message);
166
+ if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
167
+ this.handleConnectionError(err);
168
+ }
169
+ }
170
+ }
171
+ const defaultPoolOptions = {
172
+ enableKeepAlive: true,
173
+ keepAliveInitialDelay: 1e4,
174
+ connectTimeout: 2e4,
175
+ dateStrings: true,
176
+ waitForConnections: true,
177
+ connectionLimit: 10,
178
+ queueLimit: 0,
179
+ ...config.poolOptions
103
180
  };
104
- }
105
- if (dbName && serverConfig) {
181
+ let pool;
182
+ if (config.url) {
183
+ pool = import_promise.default.createPool({
184
+ uri: config.url,
185
+ ...defaultPoolOptions
186
+ });
187
+ } else {
188
+ pool = import_promise.default.createPool({
189
+ host: config.host ?? "localhost",
190
+ port: config.port ?? 3306,
191
+ user: config.user,
192
+ password: config.password,
193
+ database: config.database,
194
+ ...defaultPoolOptions
195
+ });
196
+ }
106
197
  try {
107
- const tempConn = await import_promise.default.createConnection(serverConfig);
108
- await tempConn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
109
- await tempConn.end();
110
- console.log(`[dyrected/db-mysql] Database "${dbName}" checked/created successfully`);
198
+ await this.retryOperation(async () => {
199
+ await pool.query(`
200
+ CREATE TABLE IF NOT EXISTS dyrected_internal (
201
+ \`key\` VARCHAR(255) PRIMARY KEY,
202
+ value JSON NOT NULL
203
+ )
204
+ `);
205
+ await pool.query(`
206
+ CREATE TABLE IF NOT EXISTS _dyrected_ai_threads (
207
+ id VARCHAR(36) PRIMARY KEY,
208
+ project_id VARCHAR(255) NOT NULL,
209
+ user_id VARCHAR(255) NOT NULL,
210
+ title TEXT,
211
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
212
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
213
+ INDEX idx_ai_threads_user_project (user_id, project_id)
214
+ )
215
+ `);
216
+ await pool.query(`
217
+ CREATE TABLE IF NOT EXISTS _dyrected_ai_messages (
218
+ id VARCHAR(36) PRIMARY KEY,
219
+ thread_id VARCHAR(36) NOT NULL,
220
+ role VARCHAR(50) NOT NULL,
221
+ content LONGTEXT NOT NULL,
222
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
223
+ metadata JSON,
224
+ INDEX idx_ai_messages_thread_id (thread_id),
225
+ CONSTRAINT fk_ai_messages_thread FOREIGN KEY (thread_id) REFERENCES _dyrected_ai_threads(id) ON DELETE CASCADE
226
+ )
227
+ `);
228
+ }, 3, 1e3);
111
229
  } catch (err) {
112
- console.warn(`[dyrected/db-mysql] Auto-creation of database "${dbName}" skipped/failed:`, err.message);
113
- if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
114
- this.handleConnectionError(err);
115
- }
230
+ this.handleConnectionError(err);
116
231
  }
117
- }
118
- if (config.url) {
119
- this.pool = import_promise.default.createPool(config.url);
120
- } else {
121
- this.pool = import_promise.default.createPool({
122
- host: config.host ?? "localhost",
123
- port: config.port ?? 3306,
124
- user: config.user,
125
- password: config.password,
126
- database: config.database,
127
- dateStrings: true
128
- });
129
- }
232
+ return pool;
233
+ })();
234
+ cache.set(cacheKey, { initPromise });
130
235
  try {
131
- await this.pool.query(`
132
- CREATE TABLE IF NOT EXISTS dyrected_internal (
133
- \`key\` VARCHAR(255) PRIMARY KEY,
134
- value JSON NOT NULL
135
- )
136
- `);
137
- } catch (err) {
138
- this.handleConnectionError(err);
236
+ this.pool = await initPromise;
237
+ cache.set(cacheKey, { pool: this.pool });
238
+ } catch (error) {
239
+ cache.delete(cacheKey);
240
+ throw error;
139
241
  }
140
242
  }
141
243
  async query(sql, params) {
@@ -155,28 +257,92 @@ FIX INSTRUCTIONS:
155
257
  }
156
258
  }
157
259
  getTableName(slug) {
158
- return `collection_${slug}`;
260
+ if (slug.includes(".")) {
261
+ const parts = slug.split(".");
262
+ return parts[parts.length - 1].replace(/`/g, "");
263
+ }
264
+ return slug.startsWith("collection_") ? slug : `collection_${slug}`;
159
265
  }
160
- async ensureTable(slug, fields = []) {
161
- const tableName = this.getTableName(slug);
162
- await this.query(`
163
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
164
- id VARCHAR(36) PRIMARY KEY,
165
- data JSON NOT NULL,
166
- created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
167
- updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
168
- )
169
- `);
266
+ async getTableColumns(tableName) {
267
+ const cached = this.tableColumnsCache.get(tableName);
268
+ if (cached) return cached;
170
269
  const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
171
270
  const existingCols = cols.map((c) => c.Field);
172
- for (const field of fields) {
173
- if (field.promoted && !existingCols.includes(field.name)) {
174
- console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
175
- let sqlType = "TEXT";
176
- if (field.type === "number") sqlType = "DECIMAL(19,4)";
177
- if (field.type === "boolean") sqlType = "TINYINT(1)";
178
- await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
271
+ this.tableColumnsCache.set(tableName, existingCols);
272
+ return existingCols;
273
+ }
274
+ async ensureTable(slug, fields = []) {
275
+ if (slug === "_dyrected_ai_threads" || slug === "_dyrected_ai_messages") {
276
+ return;
277
+ }
278
+ if (fields.length === 0 && this.ensuredTables.has(slug)) {
279
+ return;
280
+ }
281
+ if (this.tableLocks.has(slug)) {
282
+ await this.tableLocks.get(slug);
283
+ if (fields.length === 0 && this.ensuredTables.has(slug)) {
284
+ return;
285
+ }
286
+ }
287
+ let resolveLock;
288
+ let rejectLock;
289
+ const lockPromise = new Promise((resolve, reject) => {
290
+ resolveLock = resolve;
291
+ rejectLock = reject;
292
+ });
293
+ this.tableLocks.set(slug, lockPromise);
294
+ try {
295
+ const tableName = this.getTableName(slug);
296
+ await this.query(`
297
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
298
+ id VARCHAR(36) PRIMARY KEY,
299
+ data JSON NOT NULL,
300
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
301
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
302
+ )
303
+ `);
304
+ const existingCols = await this.getTableColumns(tableName);
305
+ for (const field of fields) {
306
+ if (field.promoted && !existingCols.includes(field.name)) {
307
+ console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
308
+ let sqlType = "TEXT";
309
+ if (field.type === "number") sqlType = "DECIMAL(19,4)";
310
+ if (field.type === "boolean") sqlType = "TINYINT(1)";
311
+ if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
312
+ try {
313
+ await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
314
+ existingCols.push(field.name);
315
+ this.tableColumnsCache.set(tableName, existingCols);
316
+ } catch (err) {
317
+ if (err.code === "ER_DUP_FIELDNAME" || err.errno === 1060 || err.message?.includes("Duplicate column name")) {
318
+ } else {
319
+ throw err;
320
+ }
321
+ }
322
+ const escapedField = field.name.replace(/`/g, "``");
323
+ let castExpr = `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}'))`;
324
+ if (field.type === "number") {
325
+ 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)`;
326
+ } else if (field.type === "boolean") {
327
+ castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) IN ('true', '1'), 1, 0)`;
328
+ } else if (field.type === "date" || field.type === "datetime") {
329
+ castExpr = `STR_TO_DATE(LEFT(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')), 23), '%Y-%m-%d %H:%i:%s.%f')`;
330
+ }
331
+ try {
332
+ await this.query(
333
+ `UPDATE \`${tableName}\` SET \`${escapedField}\` = ${castExpr} WHERE \`${escapedField}\` IS NULL AND JSON_CONTAINS_PATH(data, 'one', '$.${field.name}')`
334
+ );
335
+ } catch {
336
+ }
337
+ }
179
338
  }
339
+ this.ensuredTables.add(slug);
340
+ resolveLock();
341
+ } catch (err) {
342
+ rejectLock(err);
343
+ throw err;
344
+ } finally {
345
+ this.tableLocks.delete(slug);
180
346
  }
181
347
  }
182
348
  async find(args) {
@@ -185,8 +351,7 @@ FIX INSTRUCTIONS:
185
351
  const limit = args.limit ?? 10;
186
352
  const page = args.page ?? 1;
187
353
  const offset = (page - 1) * limit;
188
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
189
- const existingCols = cols.map((c) => c.Field);
354
+ const existingCols = await this.getTableColumns(tableName);
190
355
  let whereSql = "";
191
356
  let whereParams = [];
192
357
  if (args.where && Object.keys(args.where).length > 0) {
@@ -249,8 +414,7 @@ FIX INSTRUCTIONS:
249
414
  async create(params) {
250
415
  if (!this.inTransaction) await this.ensureTable(params.collection);
251
416
  const tableName = this.getTableName(params.collection);
252
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
253
- const existingCols = cols.map((c) => c.Field);
417
+ const existingCols = await this.getTableColumns(tableName);
254
418
  const id = params.data.id ?? Math.random().toString(36).substring(7);
255
419
  const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
256
420
  const data = { ...params.data };
@@ -273,8 +437,7 @@ FIX INSTRUCTIONS:
273
437
  async update(params) {
274
438
  if (!this.inTransaction) await this.ensureTable(params.collection);
275
439
  const tableName = this.getTableName(params.collection);
276
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
277
- const existingCols = cols.map((c) => c.Field);
440
+ const existingCols = await this.getTableColumns(tableName);
278
441
  const existing = await this.findOne({ collection: params.collection, id: params.id });
279
442
  const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
280
443
  const merged = { ...existing ?? {}, ...params.data };
@@ -349,8 +512,7 @@ FIX INSTRUCTIONS:
349
512
  }
350
513
  const tableName = this.getTableName(args.collection);
351
514
  if (!this.inTransaction) await this.ensureTable(args.collection);
352
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
353
- const existingCols = cols.map((c) => c.Field);
515
+ const existingCols = await this.getTableColumns(tableName);
354
516
  const toFieldExpr = (field) => {
355
517
  if (field === "createdAt") return "`created_at`";
356
518
  if (field === "updatedAt") return "`updated_at`";
@@ -443,6 +605,11 @@ FIX INSTRUCTIONS:
443
605
  return result;
444
606
  }
445
607
  async disconnect() {
608
+ const cache = getSharedMysqlClientCache();
609
+ const cacheKey = getMysqlCacheKey(this.config);
610
+ cache.delete(cacheKey);
611
+ this.ensuredTables.clear();
612
+ this.tableColumnsCache.clear();
446
613
  if (this.initPromise) {
447
614
  try {
448
615
  await this.initPromise;
@@ -457,9 +624,25 @@ FIX INSTRUCTIONS:
457
624
  this.pool = null;
458
625
  }
459
626
  };
627
+ async function closeAllMysqlClients() {
628
+ const cache = getSharedMysqlClientCache();
629
+ const entries = Array.from(cache.values());
630
+ cache.clear();
631
+ for (const entry of entries) {
632
+ try {
633
+ const pool = entry.pool || await entry.initPromise;
634
+ if (pool && typeof pool.end === "function") {
635
+ await pool.end().catch(() => {
636
+ });
637
+ }
638
+ } catch {
639
+ }
640
+ }
641
+ }
460
642
  var mysqlAdapter = (config) => new MysqlAdapter(config);
461
643
  // Annotate the CommonJS export names for ESM import in node:
462
644
  0 && (module.exports = {
463
645
  MysqlAdapter,
646
+ closeAllMysqlClients,
464
647
  mysqlAdapter
465
648
  });
package/dist/index.d.cts CHANGED
@@ -9,19 +9,26 @@ interface MysqlAdapterConfig {
9
9
  user?: string;
10
10
  password?: string;
11
11
  database?: string;
12
+ /** Optional pool options passed directly to mysql2 createPool */
13
+ poolOptions?: Record<string, any>;
12
14
  }
13
15
  declare class MysqlAdapter implements DatabaseAdapter {
14
16
  private pool;
15
17
  private config;
16
18
  private initPromise;
17
19
  private inTransaction;
20
+ private tableLocks;
21
+ private ensuredTables;
22
+ private tableColumnsCache;
18
23
  constructor(config: MysqlAdapterConfig);
19
24
  private handleConnectionError;
25
+ private retryOperation;
20
26
  private ensureInitialized;
21
27
  private initialize;
22
28
  private query;
23
29
  execute(sql: string, params?: any[]): Promise<any>;
24
30
  private getTableName;
31
+ private getTableColumns;
25
32
  private ensureTable;
26
33
  find(args: {
27
34
  collection: string;
@@ -65,6 +72,7 @@ declare class MysqlAdapter implements DatabaseAdapter {
65
72
  }): Promise<Record<string, any>>;
66
73
  disconnect(): Promise<void>;
67
74
  }
75
+ declare function closeAllMysqlClients(): Promise<void>;
68
76
  declare const mysqlAdapter: (config: MysqlAdapterConfig) => MysqlAdapter;
69
77
 
70
- export { MysqlAdapter, type MysqlAdapterConfig, mysqlAdapter };
78
+ export { MysqlAdapter, type MysqlAdapterConfig, closeAllMysqlClients, mysqlAdapter };
package/dist/index.d.ts CHANGED
@@ -9,19 +9,26 @@ interface MysqlAdapterConfig {
9
9
  user?: string;
10
10
  password?: string;
11
11
  database?: string;
12
+ /** Optional pool options passed directly to mysql2 createPool */
13
+ poolOptions?: Record<string, any>;
12
14
  }
13
15
  declare class MysqlAdapter implements DatabaseAdapter {
14
16
  private pool;
15
17
  private config;
16
18
  private initPromise;
17
19
  private inTransaction;
20
+ private tableLocks;
21
+ private ensuredTables;
22
+ private tableColumnsCache;
18
23
  constructor(config: MysqlAdapterConfig);
19
24
  private handleConnectionError;
25
+ private retryOperation;
20
26
  private ensureInitialized;
21
27
  private initialize;
22
28
  private query;
23
29
  execute(sql: string, params?: any[]): Promise<any>;
24
30
  private getTableName;
31
+ private getTableColumns;
25
32
  private ensureTable;
26
33
  find(args: {
27
34
  collection: string;
@@ -65,6 +72,7 @@ declare class MysqlAdapter implements DatabaseAdapter {
65
72
  }): Promise<Record<string, any>>;
66
73
  disconnect(): Promise<void>;
67
74
  }
75
+ declare function closeAllMysqlClients(): Promise<void>;
68
76
  declare const mysqlAdapter: (config: MysqlAdapterConfig) => MysqlAdapter;
69
77
 
70
- export { MysqlAdapter, type MysqlAdapterConfig, mysqlAdapter };
78
+ export { MysqlAdapter, type MysqlAdapterConfig, closeAllMysqlClients, mysqlAdapter };
package/dist/index.js CHANGED
@@ -1,6 +1,18 @@
1
1
  // src/index.ts
2
2
  import { parseSort, parseSqlWhere } from "@dyrected/core";
3
3
  import mysql from "mysql2/promise";
4
+ var MYSQL_CLIENT_CACHE_KEY = "__dyrectedMysqlClientCache";
5
+ function getSharedMysqlClientCache() {
6
+ const globalScope = globalThis;
7
+ if (!globalScope[MYSQL_CLIENT_CACHE_KEY]) {
8
+ globalScope[MYSQL_CLIENT_CACHE_KEY] = /* @__PURE__ */ new Map();
9
+ }
10
+ return globalScope[MYSQL_CLIENT_CACHE_KEY];
11
+ }
12
+ function getMysqlCacheKey(config) {
13
+ if (config.url) return config.url;
14
+ return `${config.host || "localhost"}:${config.port || 3306}:${config.database || ""}:${config.user || ""}`;
15
+ }
4
16
  function escapeMysqlIdentifier(identifier) {
5
17
  return `\`${identifier.replace(/`/g, "``")}\``;
6
18
  }
@@ -19,6 +31,9 @@ var MysqlAdapter = class {
19
31
  config;
20
32
  initPromise = null;
21
33
  inTransaction = false;
34
+ tableLocks = /* @__PURE__ */ new Map();
35
+ ensuredTables = /* @__PURE__ */ new Set();
36
+ tableColumnsCache = /* @__PURE__ */ new Map();
22
37
  constructor(config) {
23
38
  this.config = config;
24
39
  }
@@ -41,6 +56,25 @@ FIX INSTRUCTIONS:
41
56
  }
42
57
  throw err;
43
58
  }
59
+ async retryOperation(operation, maxRetries = 3, initialDelay = 1e3) {
60
+ let lastError;
61
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
62
+ try {
63
+ return await operation();
64
+ } catch (err) {
65
+ lastError = err;
66
+ const isNetworkOrTimeout = err.code === "ETIMEDOUT" || err.code === "ECONNREFUSED" || err.code === "PROTOCOL_CONNECTION_LOST" || err.message?.includes("ETIMEDOUT") || err.message?.includes("Connection lost");
67
+ if (attempt < maxRetries && isNetworkOrTimeout) {
68
+ const delay = initialDelay * Math.pow(2, attempt - 1);
69
+ console.warn(`[dyrected/db-mysql] Connection attempt ${attempt} failed (${err.code || err.message}), retrying in ${delay}ms...`);
70
+ await new Promise((resolve) => setTimeout(resolve, delay));
71
+ } else {
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ throw lastError;
77
+ }
44
78
  async ensureInitialized() {
45
79
  if (!this.initPromise) {
46
80
  this.initPromise = this.initialize();
@@ -48,59 +82,126 @@ FIX INSTRUCTIONS:
48
82
  await this.initPromise;
49
83
  }
50
84
  async initialize() {
51
- const config = this.config;
52
- let dbName = config.database;
53
- let serverConfig = null;
54
- if (config.url) {
55
- try {
56
- const parsed = new URL(config.url);
57
- dbName = parsed.pathname.replace(/^\//, "");
58
- const serverUrl = `${parsed.protocol}//${parsed.username}:${parsed.password}@${parsed.host}`;
59
- serverConfig = serverUrl;
60
- } catch (err) {
85
+ const cache = getSharedMysqlClientCache();
86
+ const cacheKey = getMysqlCacheKey(this.config);
87
+ const cached = cache.get(cacheKey);
88
+ if (cached?.pool) {
89
+ this.pool = cached.pool;
90
+ return;
91
+ }
92
+ if (cached?.initPromise) {
93
+ this.pool = await cached.initPromise;
94
+ return;
95
+ }
96
+ const initPromise = (async () => {
97
+ const config = this.config;
98
+ let dbName = config.database;
99
+ let serverConfig = null;
100
+ if (config.url) {
101
+ try {
102
+ const parsed = new URL(config.url);
103
+ dbName = parsed.pathname.replace(/^\//, "");
104
+ const serverUrl = `${parsed.protocol}//${parsed.username}:${parsed.password}@${parsed.host}`;
105
+ serverConfig = serverUrl;
106
+ } catch (err) {
107
+ }
108
+ } else {
109
+ serverConfig = {
110
+ host: config.host ?? "localhost",
111
+ port: config.port ?? 3306,
112
+ user: config.user,
113
+ password: config.password
114
+ };
61
115
  }
62
- } else {
63
- serverConfig = {
64
- host: config.host ?? "localhost",
65
- port: config.port ?? 3306,
66
- user: config.user,
67
- password: config.password
116
+ if (dbName && serverConfig) {
117
+ try {
118
+ await this.retryOperation(async () => {
119
+ const tempConn = await mysql.createConnection(serverConfig);
120
+ try {
121
+ await tempConn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
122
+ } finally {
123
+ await tempConn.end().catch(() => {
124
+ });
125
+ }
126
+ }, 3, 1e3);
127
+ console.log(`[dyrected/db-mysql] Database "${dbName}" checked/created successfully`);
128
+ } catch (err) {
129
+ console.warn(`[dyrected/db-mysql] Auto-creation of database "${dbName}" skipped/failed:`, err.message);
130
+ if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
131
+ this.handleConnectionError(err);
132
+ }
133
+ }
134
+ }
135
+ const defaultPoolOptions = {
136
+ enableKeepAlive: true,
137
+ keepAliveInitialDelay: 1e4,
138
+ connectTimeout: 2e4,
139
+ dateStrings: true,
140
+ waitForConnections: true,
141
+ connectionLimit: 10,
142
+ queueLimit: 0,
143
+ ...config.poolOptions
68
144
  };
69
- }
70
- if (dbName && serverConfig) {
145
+ let pool;
146
+ if (config.url) {
147
+ pool = mysql.createPool({
148
+ uri: config.url,
149
+ ...defaultPoolOptions
150
+ });
151
+ } else {
152
+ pool = mysql.createPool({
153
+ host: config.host ?? "localhost",
154
+ port: config.port ?? 3306,
155
+ user: config.user,
156
+ password: config.password,
157
+ database: config.database,
158
+ ...defaultPoolOptions
159
+ });
160
+ }
71
161
  try {
72
- const tempConn = await mysql.createConnection(serverConfig);
73
- await tempConn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
74
- await tempConn.end();
75
- console.log(`[dyrected/db-mysql] Database "${dbName}" checked/created successfully`);
162
+ await this.retryOperation(async () => {
163
+ await pool.query(`
164
+ CREATE TABLE IF NOT EXISTS dyrected_internal (
165
+ \`key\` VARCHAR(255) PRIMARY KEY,
166
+ value JSON NOT NULL
167
+ )
168
+ `);
169
+ await pool.query(`
170
+ CREATE TABLE IF NOT EXISTS _dyrected_ai_threads (
171
+ id VARCHAR(36) PRIMARY KEY,
172
+ project_id VARCHAR(255) NOT NULL,
173
+ user_id VARCHAR(255) NOT NULL,
174
+ title TEXT,
175
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
176
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
177
+ INDEX idx_ai_threads_user_project (user_id, project_id)
178
+ )
179
+ `);
180
+ await pool.query(`
181
+ CREATE TABLE IF NOT EXISTS _dyrected_ai_messages (
182
+ id VARCHAR(36) PRIMARY KEY,
183
+ thread_id VARCHAR(36) NOT NULL,
184
+ role VARCHAR(50) NOT NULL,
185
+ content LONGTEXT NOT NULL,
186
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
187
+ metadata JSON,
188
+ INDEX idx_ai_messages_thread_id (thread_id),
189
+ CONSTRAINT fk_ai_messages_thread FOREIGN KEY (thread_id) REFERENCES _dyrected_ai_threads(id) ON DELETE CASCADE
190
+ )
191
+ `);
192
+ }, 3, 1e3);
76
193
  } catch (err) {
77
- console.warn(`[dyrected/db-mysql] Auto-creation of database "${dbName}" skipped/failed:`, err.message);
78
- if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
79
- this.handleConnectionError(err);
80
- }
194
+ this.handleConnectionError(err);
81
195
  }
82
- }
83
- if (config.url) {
84
- this.pool = mysql.createPool(config.url);
85
- } else {
86
- this.pool = mysql.createPool({
87
- host: config.host ?? "localhost",
88
- port: config.port ?? 3306,
89
- user: config.user,
90
- password: config.password,
91
- database: config.database,
92
- dateStrings: true
93
- });
94
- }
196
+ return pool;
197
+ })();
198
+ cache.set(cacheKey, { initPromise });
95
199
  try {
96
- await this.pool.query(`
97
- CREATE TABLE IF NOT EXISTS dyrected_internal (
98
- \`key\` VARCHAR(255) PRIMARY KEY,
99
- value JSON NOT NULL
100
- )
101
- `);
102
- } catch (err) {
103
- this.handleConnectionError(err);
200
+ this.pool = await initPromise;
201
+ cache.set(cacheKey, { pool: this.pool });
202
+ } catch (error) {
203
+ cache.delete(cacheKey);
204
+ throw error;
104
205
  }
105
206
  }
106
207
  async query(sql, params) {
@@ -120,28 +221,92 @@ FIX INSTRUCTIONS:
120
221
  }
121
222
  }
122
223
  getTableName(slug) {
123
- return `collection_${slug}`;
224
+ if (slug.includes(".")) {
225
+ const parts = slug.split(".");
226
+ return parts[parts.length - 1].replace(/`/g, "");
227
+ }
228
+ return slug.startsWith("collection_") ? slug : `collection_${slug}`;
124
229
  }
125
- async ensureTable(slug, fields = []) {
126
- const tableName = this.getTableName(slug);
127
- await this.query(`
128
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
129
- id VARCHAR(36) PRIMARY KEY,
130
- data JSON NOT NULL,
131
- created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
132
- updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
133
- )
134
- `);
230
+ async getTableColumns(tableName) {
231
+ const cached = this.tableColumnsCache.get(tableName);
232
+ if (cached) return cached;
135
233
  const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
136
234
  const existingCols = cols.map((c) => c.Field);
137
- for (const field of fields) {
138
- if (field.promoted && !existingCols.includes(field.name)) {
139
- console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
140
- let sqlType = "TEXT";
141
- if (field.type === "number") sqlType = "DECIMAL(19,4)";
142
- if (field.type === "boolean") sqlType = "TINYINT(1)";
143
- await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
235
+ this.tableColumnsCache.set(tableName, existingCols);
236
+ return existingCols;
237
+ }
238
+ async ensureTable(slug, fields = []) {
239
+ if (slug === "_dyrected_ai_threads" || slug === "_dyrected_ai_messages") {
240
+ return;
241
+ }
242
+ if (fields.length === 0 && this.ensuredTables.has(slug)) {
243
+ return;
244
+ }
245
+ if (this.tableLocks.has(slug)) {
246
+ await this.tableLocks.get(slug);
247
+ if (fields.length === 0 && this.ensuredTables.has(slug)) {
248
+ return;
249
+ }
250
+ }
251
+ let resolveLock;
252
+ let rejectLock;
253
+ const lockPromise = new Promise((resolve, reject) => {
254
+ resolveLock = resolve;
255
+ rejectLock = reject;
256
+ });
257
+ this.tableLocks.set(slug, lockPromise);
258
+ try {
259
+ const tableName = this.getTableName(slug);
260
+ await this.query(`
261
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
262
+ id VARCHAR(36) PRIMARY KEY,
263
+ data JSON NOT NULL,
264
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
265
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
266
+ )
267
+ `);
268
+ const existingCols = await this.getTableColumns(tableName);
269
+ for (const field of fields) {
270
+ if (field.promoted && !existingCols.includes(field.name)) {
271
+ console.log(`[dyrected/mysql] Promoting field "${field.name}" to column in ${tableName}`);
272
+ let sqlType = "TEXT";
273
+ if (field.type === "number") sqlType = "DECIMAL(19,4)";
274
+ if (field.type === "boolean") sqlType = "TINYINT(1)";
275
+ if (field.type === "date" || field.type === "datetime") sqlType = "DATETIME(3)";
276
+ try {
277
+ await this.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.name}\` ${sqlType}`);
278
+ existingCols.push(field.name);
279
+ this.tableColumnsCache.set(tableName, existingCols);
280
+ } catch (err) {
281
+ if (err.code === "ER_DUP_FIELDNAME" || err.errno === 1060 || err.message?.includes("Duplicate column name")) {
282
+ } else {
283
+ throw err;
284
+ }
285
+ }
286
+ const escapedField = field.name.replace(/`/g, "``");
287
+ let castExpr = `JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}'))`;
288
+ if (field.type === "number") {
289
+ 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)`;
290
+ } else if (field.type === "boolean") {
291
+ castExpr = `IF(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')) IN ('true', '1'), 1, 0)`;
292
+ } else if (field.type === "date" || field.type === "datetime") {
293
+ castExpr = `STR_TO_DATE(LEFT(JSON_UNQUOTE(JSON_EXTRACT(data, '$.${field.name}')), 23), '%Y-%m-%d %H:%i:%s.%f')`;
294
+ }
295
+ try {
296
+ await this.query(
297
+ `UPDATE \`${tableName}\` SET \`${escapedField}\` = ${castExpr} WHERE \`${escapedField}\` IS NULL AND JSON_CONTAINS_PATH(data, 'one', '$.${field.name}')`
298
+ );
299
+ } catch {
300
+ }
301
+ }
144
302
  }
303
+ this.ensuredTables.add(slug);
304
+ resolveLock();
305
+ } catch (err) {
306
+ rejectLock(err);
307
+ throw err;
308
+ } finally {
309
+ this.tableLocks.delete(slug);
145
310
  }
146
311
  }
147
312
  async find(args) {
@@ -150,8 +315,7 @@ FIX INSTRUCTIONS:
150
315
  const limit = args.limit ?? 10;
151
316
  const page = args.page ?? 1;
152
317
  const offset = (page - 1) * limit;
153
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
154
- const existingCols = cols.map((c) => c.Field);
318
+ const existingCols = await this.getTableColumns(tableName);
155
319
  let whereSql = "";
156
320
  let whereParams = [];
157
321
  if (args.where && Object.keys(args.where).length > 0) {
@@ -214,8 +378,7 @@ FIX INSTRUCTIONS:
214
378
  async create(params) {
215
379
  if (!this.inTransaction) await this.ensureTable(params.collection);
216
380
  const tableName = this.getTableName(params.collection);
217
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
218
- const existingCols = cols.map((c) => c.Field);
381
+ const existingCols = await this.getTableColumns(tableName);
219
382
  const id = params.data.id ?? Math.random().toString(36).substring(7);
220
383
  const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
221
384
  const data = { ...params.data };
@@ -238,8 +401,7 @@ FIX INSTRUCTIONS:
238
401
  async update(params) {
239
402
  if (!this.inTransaction) await this.ensureTable(params.collection);
240
403
  const tableName = this.getTableName(params.collection);
241
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
242
- const existingCols = cols.map((c) => c.Field);
404
+ const existingCols = await this.getTableColumns(tableName);
243
405
  const existing = await this.findOne({ collection: params.collection, id: params.id });
244
406
  const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", "");
245
407
  const merged = { ...existing ?? {}, ...params.data };
@@ -314,8 +476,7 @@ FIX INSTRUCTIONS:
314
476
  }
315
477
  const tableName = this.getTableName(args.collection);
316
478
  if (!this.inTransaction) await this.ensureTable(args.collection);
317
- const [cols] = await this.query(`SHOW COLUMNS FROM \`${tableName}\``);
318
- const existingCols = cols.map((c) => c.Field);
479
+ const existingCols = await this.getTableColumns(tableName);
319
480
  const toFieldExpr = (field) => {
320
481
  if (field === "createdAt") return "`created_at`";
321
482
  if (field === "updatedAt") return "`updated_at`";
@@ -408,6 +569,11 @@ FIX INSTRUCTIONS:
408
569
  return result;
409
570
  }
410
571
  async disconnect() {
572
+ const cache = getSharedMysqlClientCache();
573
+ const cacheKey = getMysqlCacheKey(this.config);
574
+ cache.delete(cacheKey);
575
+ this.ensuredTables.clear();
576
+ this.tableColumnsCache.clear();
411
577
  if (this.initPromise) {
412
578
  try {
413
579
  await this.initPromise;
@@ -422,8 +588,24 @@ FIX INSTRUCTIONS:
422
588
  this.pool = null;
423
589
  }
424
590
  };
591
+ async function closeAllMysqlClients() {
592
+ const cache = getSharedMysqlClientCache();
593
+ const entries = Array.from(cache.values());
594
+ cache.clear();
595
+ for (const entry of entries) {
596
+ try {
597
+ const pool = entry.pool || await entry.initPromise;
598
+ if (pool && typeof pool.end === "function") {
599
+ await pool.end().catch(() => {
600
+ });
601
+ }
602
+ } catch {
603
+ }
604
+ }
605
+ }
425
606
  var mysqlAdapter = (config) => new MysqlAdapter(config);
426
607
  export {
427
608
  MysqlAdapter,
609
+ closeAllMysqlClients,
428
610
  mysqlAdapter
429
611
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/db-mysql",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
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.12.0"
21
+ "@dyrected/core": "^2.13.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "tsup": "^8.0.0",