@dyrected/db-mysql 2.13.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/LICENSE.md +50 -0
- package/dist/index.cjs +256 -73
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +255 -73
- package/package.json +7 -7
package/LICENSE.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: Dyrected
|
|
6
|
+
Licensed Work: Dyrected
|
|
7
|
+
Additional Use Grant: Production use and commercial use of the Licensed Work are permitted for your own internal business operations, your own websites and applications, and client websites or applications that you build or deploy on behalf of a client, provided that you do not, without a separate commercial license from the Licensor, (a) provide the Licensed Work as a hosted or managed service to third parties, (b) embed, white-label, sublicense, or otherwise redistribute the Licensed Work as part of a multi-tenant or customer-facing SaaS, platform, or developer service, or (c) enable third parties to create, manage, or operate independent projects, sites, workspaces, or environments using the Licensed Work as a material part of the service you provide.
|
|
8
|
+
Change Date: 2030-05-18
|
|
9
|
+
Change License: Apache License 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Business Source License 1.1
|
|
14
|
+
|
|
15
|
+
License text copyright © 2024 MariaDB plc, All Rights Reserved.
|
|
16
|
+
“Business Source License” is a trademark of MariaDB plc.
|
|
17
|
+
|
|
18
|
+
### Terms
|
|
19
|
+
|
|
20
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
21
|
+
|
|
22
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.
|
|
23
|
+
|
|
24
|
+
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
|
|
25
|
+
|
|
26
|
+
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor.
|
|
27
|
+
|
|
28
|
+
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.
|
|
29
|
+
|
|
30
|
+
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.
|
|
31
|
+
|
|
32
|
+
This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License).
|
|
33
|
+
|
|
34
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
|
|
35
|
+
|
|
36
|
+
MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the trademark “Business Source License”, as long as you comply with the Covenants of Licensor below.
|
|
37
|
+
|
|
38
|
+
### Covenants of Licensor
|
|
39
|
+
|
|
40
|
+
In consideration of the right to use this License’s text and the “Business Source License” name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor:
|
|
41
|
+
|
|
42
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where “compatible” means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation.
|
|
43
|
+
|
|
44
|
+
2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text “None”.
|
|
45
|
+
|
|
46
|
+
3. Not to modify this License in any other way.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
The Business Source License (this document, or the “License”) is not an Open Source license. However, the Licensed Work will eventually be made available under an Open Source License, as stated in this License.
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
|
|
114
|
-
this.handleConnectionError(err);
|
|
115
|
-
}
|
|
230
|
+
this.handleConnectionError(err);
|
|
116
231
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
const
|
|
162
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
if (err.code === "EADDRNOTAVAIL" || err.message?.includes("EADDRNOTAVAIL")) {
|
|
79
|
-
this.handleConnectionError(err);
|
|
80
|
-
}
|
|
194
|
+
this.handleConnectionError(err);
|
|
81
195
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.13.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "MySQL adapter for Dyrected CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,13 +16,9 @@
|
|
|
16
16
|
"require": "./dist/index.cjs"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
21
|
-
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
22
|
-
},
|
|
23
19
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
20
|
+
"mysql2": "^3.22.3",
|
|
21
|
+
"@dyrected/core": "^2.13.1"
|
|
26
22
|
},
|
|
27
23
|
"devDependencies": {
|
|
28
24
|
"tsup": "^8.0.0",
|
|
@@ -36,5 +32,9 @@
|
|
|
36
32
|
},
|
|
37
33
|
"publishConfig": {
|
|
38
34
|
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
38
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
39
39
|
}
|
|
40
40
|
}
|