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": [