diff --git a/MySQL.lua b/MySQL.lua
index 49d8a1b..cdb2361 100644
--- a/MySQL.lua
+++ b/MySQL.lua
@@ -30,11 +30,13 @@ local rawget = rawget
local next = next
local setmetatable = setmetatable
local GetResourceState = GetResourceState
+local GetCurrentResourceName = GetCurrentResourceName
local CreateThread = Citizen.CreateThread
local Wait = Citizen.Wait
-
-local export = exports['fivem-mysql']
-local mysql = setmetatable({}, {})
+local mysql = setmetatable({
+ resource_name = 'fivem-mysql',
+ current_resource_name = GetCurrentResourceName()
+}, {})
function mysql:typeof(input)
if (input == nil) then
@@ -70,45 +72,61 @@ end
function mysql:insert(query, params)
params = params or {}
- assert(self:typeof(query) == 'string', 'SQL query must be a string')
- assert(self:typeof(params) == 'table', 'Parameters must be a table')
+ local res, finished = nil, false
- params = self:safeParams(params)
+ self:insertAsync(query, params, function(result)
+ res = result
+ finished = true
+ end)
+
+ repeat Citizen.Wait(0) until finished == true
- return export:insert(query, params)
+ return res
end
function mysql:fetchAll(query, params)
params = params or {}
- assert(self:typeof(query) == 'string', 'SQL query must be a string')
- assert(self:typeof(params) == 'table', 'Parameters must be a table')
+ local res, finished = nil, false
- params = self:safeParams(params)
+ self:fetchAllAsync(query, params, function(result)
+ res = result
+ finished = true
+ end)
+
+ repeat Citizen.Wait(0) until finished == true
- return export:fetchAll(query, params)
+ return res
end
function mysql:fetchScalar(query, params)
params = params or {}
- assert(self:typeof(query) == 'string', 'SQL query must be a string')
- assert(self:typeof(params) == 'table', 'Parameters must be a table')
+ local res, finished = nil, false
- params = self:safeParams(params)
+ self:fetchScalarAsync(query, params, function(result)
+ res = result
+ finished = true
+ end)
+
+ repeat Citizen.Wait(0) until finished == true
- return export:fetchScalar(query, params)
+ return res
end
function mysql:fetchFirst(query, params)
params = params or {}
- assert(self:typeof(query) == 'string', 'SQL query must be a string')
- assert(self:typeof(params) == 'table', 'Parameters must be a table')
+ local res, finished = nil, false
- params = self:safeParams(params)
+ self:fetchFirstAsync(query, params, function(result)
+ res = result
+ finished = true
+ end)
+
+ repeat Citizen.Wait(0) until finished == true
- return export:fetchFirst(query, params)
+ return res
end
function mysql:insertAsync(query, params, callback)
@@ -120,7 +138,7 @@ function mysql:insertAsync(query, params, callback)
params = self:safeParams(params)
- export:insertAsync(query, params, callback)
+ exports[self.resource_name]:insertAsync(query, params, callback, self.current_resource_name)
end
function mysql:fetchAllAsync(query, params, callback)
@@ -132,7 +150,7 @@ function mysql:fetchAllAsync(query, params, callback)
params = self:safeParams(params)
- export:fetchAllAsync(query, params, callback)
+ exports[self.resource_name]:fetchAllAsync(query, params, callback, self.current_resource_name)
end
function mysql:fetchScalarAsync(query, params, callback)
@@ -144,7 +162,7 @@ function mysql:fetchScalarAsync(query, params, callback)
params = self:safeParams(params)
- export:fetchScalarAsync(query, params, callback)
+ exports[self.resource_name]:fetchScalarAsync(query, params, callback, self.current_resource_name)
end
function mysql:fetchFirstAsync(query, params, callback)
@@ -156,7 +174,7 @@ function mysql:fetchFirstAsync(query, params, callback)
params = self:safeParams(params)
- export:fetchFirstAsync(query, params, callback)
+ exports[self.resource_name]:fetchFirstAsync(query, params, callback, self.current_resource_name)
end
function mysql:ready(callback)
@@ -165,8 +183,8 @@ function mysql:ready(callback)
assert(self:typeof(cb) == 'function', 'Callback must be a function')
- while GetResourceState('fivem-mysql') ~= 'started' do Wait(0) end
- while not export:isReady() do Wait(0) end
+ while GetResourceState(self.resource_name) ~= 'started' do Wait(0) end
+ while not exports[self.resource_name]:isReady() do Wait(0) end
cb()
end)
diff --git a/package.json b/package.json
index e032127..16b3f8e 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"pg-connection-string": "^2.4.0",
"qs": "^6.9.4",
"sqlstring": "^2.3.2",
+ "tracer": "^1.1.4",
"ts-node": "^9.1.1"
},
"devDependencies": {
diff --git a/source/fivem/callback.ts b/source/fivem/callback.ts
index 654a959..b74bece 100644
--- a/source/fivem/callback.ts
+++ b/source/fivem/callback.ts
@@ -30,7 +30,7 @@
import { OkPacket, RowDataPacket, ResultSetHeader } from 'mysql2';
declare interface CFXCallback {
- (result: RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader | boolean | number | any | any[]): void;
+ (result: RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader | boolean | number | any | any[], query?: string): void;
};
export {
diff --git a/source/mysql/helpers.ts b/source/mysql/helpers.ts
index 2374622..1ffb5ce 100644
--- a/source/mysql/helpers.ts
+++ b/source/mysql/helpers.ts
@@ -27,6 +27,7 @@
┻
*/
+import { Tracer } from 'tracer';
import { escape } from 'sqlstring';
function fixQuery(query: string) {
@@ -53,7 +54,16 @@ function fixParameters(params: { [key: string]: any }, stringifyObjects?: boolea
return result;
}
-export default {
+function warnIfNeeded(time: [number, number], logger: Tracer.Logger, sql: string, resource: string, interval: number) {
+ const queryTime = time[0] * 1e3 + time[1] * 1e-6;
+
+ if (interval <= 0 || interval > queryTime) { return; }
+
+ logger.warn(`Resource '${resource}' executed an query that took ${queryTime.toFixed()}ms to execute\n> ^4Query: ^7${sql}\n> ^4Execution time: ^7${queryTime.toFixed()}ms`);
+}
+
+export {
fixQuery,
- fixParameters
+ fixParameters,
+ warnIfNeeded
}
\ No newline at end of file
diff --git a/source/mysql/mysql.ts b/source/mysql/mysql.ts
index d5d9553..583912d 100644
--- a/source/mysql/mysql.ts
+++ b/source/mysql/mysql.ts
@@ -28,7 +28,8 @@
*/
import { CFXCallback, OkPacket, RowDataPacket, ResultSetHeader } from '../fivem/callback';
-import MySQLHelper from './helpers';
+import { fixParameters, fixQuery } from './helpers';
+import { Tracer } from 'tracer';
import { Pool, PoolOptions, ConnectionOptions, QueryError, createPool } from 'mysql2';
declare type keyValue = { [key: string]: any };
@@ -37,8 +38,10 @@ class MySQLServer {
ready: boolean = false;
options: PoolOptions;
pool: Pool;
+ logger: Tracer.Logger;
- constructor(connectionOptions: ConnectionOptions, readyCallback?: Function) {
+ constructor(connectionOptions: ConnectionOptions, logger: Tracer.Logger, readyCallback?: Function) {
+ this.logger = logger;
this.options = {
...connectionOptions,
...{
@@ -58,20 +61,15 @@ class MySQLServer {
}
}
- beginTransaction(callback: CFXCallback) {
+ beginTransaction(callback: CFXCallback, resource: string) {
return this.pool.beginTransaction((err) => {
- if (err) {
- callback(false);
- return;
- }
-
- callback([]);
+ err ? this.errorCallback(err, callback, resource) : callback([]);
});
}
- commit(callback: CFXCallback) {
+ commit(callback: CFXCallback, resource: string) {
return this.pool.commit((err) => {
- err ? callback(false) : callback([]);
+ err ? this.errorCallback(err, callback, resource) : callback([]);
});
}
@@ -79,24 +77,30 @@ class MySQLServer {
return this.pool.rollback(() => callback([]));
}
- end() { this.pool?.end(); }
+ end(resource: string) {
+ this.pool?.end((err) => {
+ if (err) {
+ this.logger.error(`Resource '${resource}' throw an SQL error\n> ^1Message: ^7${err.message}`);
+ }
+ });
+ }
- execute(query: string, parameters: keyValue, callback: CFXCallback) {
+ execute(query: string, parameters: keyValue, callback: CFXCallback, resource: string) {
const config = this.pool?.config;
- parameters = MySQLHelper.fixParameters(parameters, config?.stringifyObjects, config?.timezone);
- query = MySQLHelper.fixQuery(query);
+ parameters = fixParameters(parameters, config?.stringifyObjects, config?.timezone);
+ query = fixQuery(query);
const sql = this.pool?.format(query, parameters);
return this.pool?.query(sql, parameters, (err, result) => {
- err ? this.errorCallback(err, callback) : callback(result);
+ err ? this.errorCallback(err, callback, resource, query) : callback(result, query);
});
}
- errorCallback(error: QueryError, callback: CFXCallback, rollback?: boolean) {
- rollback ? this.pool.rollback(() => callback([])) : callback([]);
- console.error(error.message);
+ errorCallback(error: QueryError, callback: CFXCallback, resource: string, query?: string) {
+ this.logger.error(`Resource '${resource}' throw an SQL error\n> ^1Message: ^7${error.message}`);
+ callback([], query);
}
isReady() { return this.ready; }
diff --git a/source/server.ts b/source/server.ts
index 5d4b615..6f65b53 100644
--- a/source/server.ts
+++ b/source/server.ts
@@ -26,6 +26,10 @@
┃ along with this program. If not, see .
┻
*/
+
+import { console } from 'tracer';
+import { warnIfNeeded } from './mysql/helpers';
+import { GetLoggerConfig, GetSlowQueryWarning } from './tracer';
import { MySQLServer, CFXCallback, OkPacket, ConnectionString, keyValue } from './mysql';
let isReady = false;
@@ -34,83 +38,50 @@ global.exports('isReady', (): boolean => { return isReady; });
const rawConnectionString = GetConvar('mysql_connection_string', 'mysql://root@localhost/fivem');
const connectionString = ConnectionString(rawConnectionString);
-const server = new MySQLServer(connectionString, () => { isReady = true; });
-const wait = (ms: number) => new Promise(res => setTimeout(res, ms));
-
-global.exports('insertAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
- server.execute(query, parameters, (result) => {
- callback((result)?.insertId ?? 0);
- });
-});
-
-global.exports('fetchAllAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
- server.execute(query, parameters, callback);
-});
+const slowQueryWarning = GetSlowQueryWarning();
+const logger = console(GetLoggerConfig());
+const server = new MySQLServer(connectionString, logger, () => { isReady = true; });
-global.exports('fetchScalarAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
- server.execute(query, parameters, (result) => {
- callback((result && result[0]) ? (Object.values(result[0])[0] ?? null) : null);
- });
-});
+global.exports('insertAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
+ const startTime = process.hrtime();
-global.exports('fetchFirstAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
- server.execute(query, parameters, (result) => {
- callback((result && result[0]) ? result[0] ?? [] : []);
- });
-});
+ resource = resource ?? GetInvokingResource();
-global.exports('insert', async (query: string, parameters?: keyValue): Promise => {
- let res: number = null;
- let done: boolean = false;
-
- server.execute(query, parameters, (result) => {
- res = (result?.insertId ?? 0);
- done = true;
- });
-
- do { await wait(0); } while (done == false);
-
- return res;
+ server.execute(query, parameters, (result, sql) => {
+ warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
+ callback((result)?.insertId ?? 0);
+ }, resource);
});
-global.exports('fetchAll', async (query: string, parameters?: keyValue): Promise => {
- let res: any = null;
- let done: boolean = false;
-
- server.execute(query, parameters, (result) => {
- res = result;
- done = true;
- });
+global.exports('fetchAllAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
+ const startTime = process.hrtime();
- do { await wait(0); } while (done == false);
+ resource = resource ?? GetInvokingResource();
- return res;
+ server.execute(query, parameters, (result, sql) => {
+ warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
+ callback(result);
+ }, resource);
});
-global.exports('fetchScalar', async (query: string, parameters?: keyValue): Promise => {
- let res: any = null;
- let done: boolean = false;
+global.exports('fetchScalarAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
+ const startTime = process.hrtime();
- server.execute(query, parameters, (result) => {
- res = (result && result[0]) ? (Object.values(result[0])[0] ?? null) : null;
- done = true;
- });
+ resource = resource ?? GetInvokingResource();
- do { await wait(0); } while (done == false);
-
- return res;
+ server.execute(query, parameters, (result, sql) => {
+ warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
+ callback((result && result[0]) ? (Object.values(result[0])[0] ?? null) : null);
+ }, resource);
});
-global.exports('fetchFirst', async (query: string, parameters?: keyValue): Promise => {
- let res: any = null;
- let done: boolean = false;
+global.exports('fetchFirstAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
+ const startTime = process.hrtime();
- server.execute(query, parameters, (result) => {
- res = (result && result[0]) ? result[0] ?? [] : [];
- done = true;
- });
+ resource = resource ?? GetInvokingResource();
- do { await wait(0); } while (done == false);
-
- return res;
+ server.execute(query, parameters, (result, sql) => {
+ warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
+ callback((result && result[0]) ? result[0] ?? [] : []);
+ }, resource);
});
\ No newline at end of file
diff --git a/source/tracer/index.ts b/source/tracer/index.ts
new file mode 100644
index 0000000..4daa158
--- /dev/null
+++ b/source/tracer/index.ts
@@ -0,0 +1,63 @@
+/**
+𝗙𝗶𝘃𝗲𝗠 𝗠𝘆𝗦𝗤𝗟 - 𝗠𝘆𝗦𝗤𝗟 𝗹𝗶𝗯𝗿𝗮𝗿𝘆 𝗳𝗼𝗿 𝗙𝗶𝘃𝗲𝗠
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+➤ License: https://choosealicense.com/licenses/gpl-3.0/
+➤ GitHub: https://github.com/ThymonA/fivem-mysql/
+➤ Author: Thymon Arens
+➤ Name: FiveM MySQL
+➤ Version: 1.0.0
+➤ Description: MySQL library made for FiveM
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+𝗚𝗡𝗨 𝗚𝗲𝗻𝗲𝗿𝗮𝗹 𝗣𝘂𝗯𝗹𝗶𝗰 𝗟𝗶𝗰𝗲𝗻𝘀𝗲 𝘃𝟯.𝟬
+┳
+┃ Copyright (C) 2020 Thymon Arens
+┃
+┃ This program is free software: you can redistribute it and/or modify
+┃ it under the terms of the GNU General Public License as published by
+┃ the Free Software Foundation, either version 3 of the License, or
+┃ (at your option) any later version.
+┃
+┃ This program is distributed in the hope that it will be useful,
+┃ but WITHOUT ANY WARRANTY; without even the implied warranty of
+┃ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+┃ GNU General Public License for more details.
+┃
+┃ You should have received a copy of the GNU General Public License
+┃ along with this program. If not, see .
+┻
+*/
+
+import { Tracer } from 'tracer';
+
+const resource_name = GetCurrentResourceName();
+
+function GetLoggerConfig(): Tracer.LoggerConfig {
+ return {
+ format: [
+ `^7[^4${resource_name}^7][^7{{title}}^7] ^7{{message}}^7`,
+ {
+ warn: `^7[^4${resource_name}^7][^3{{title}}^7] ^3{{message}}^7`,
+ error: `^7[^4${resource_name}^7][^1{{title}}^7] ^1{{message}}^7`,
+ fatal: `^7[^4${resource_name}^7][^1{{title}}^7] ^1{{message}}^7`
+ }
+ ],
+ level: GetConvar('mysql_level', 'warn'),
+ inspectOpt: {
+ showHidden: false,
+ depth: 0
+ },
+ rootDir: GetResourcePath(GetCurrentResourceName())
+ }
+}
+
+function GetSlowQueryWarning(): number {
+ const rawInterval = GetConvar('mysql_slow_query_warning', '500') || '500';
+ const interval = parseInt(rawInterval);
+
+ return interval > 0 ? interval : -1;
+}
+
+export {
+ GetLoggerConfig,
+ GetSlowQueryWarning
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 7883f9a..92a16bb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,8 +7,10 @@
"include": [
"source/mysql/**/*",
"source/fivem/**/*",
+ "source/tracer/**/*",
"source/mysql/*",
"source/fivem/*",
+ "source/tracer/*",
"source/server.ts"
],
"exclude": [