fix(qb-core): post-update recovery + centralizare notify 17mov_Hud
Restaurat jobs.lua din git (Quasar fork a suprascris joburile 17mov). Adăugat item map în items.lua (lipsea, rupt rv-maphold). Setat licences.driver = false în config.lua. Override QBCore.Functions.Notify + QBCore:Notify event → 17mov_Hud:ShowNotification (toate notificările merg automat prin 17mov_Hud).
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ko_fi: thelindat
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Issue checklist
|
||||
> Please put `x` inside of the box that matches your issue.
|
||||
|
||||
- [ ] I am using the latest release
|
||||
- [ ] I have referenced previously reported issues
|
||||
- [ ] I have referenced available documentation and cannot resolve the issue
|
||||
- [ ] I'm certain this is an issue with oxymysql, and not with my resource
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Code**
|
||||
```lua
|
||||
-- If applicable, include the query and relevant code
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Server details**
|
||||
- FXServer artifact
|
||||
- Operating system
|
||||
|
||||
**Database details**
|
||||
- MariaDB or MySQL
|
||||
- Version
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '32 5 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Pre-Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Version tag'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Build and Create Tagged Pre release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install archive tools
|
||||
run: sudo apt install zip
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Run build
|
||||
run: pnpm build
|
||||
|
||||
- name: Bundle files
|
||||
run: |
|
||||
mkdir -p ./temp/oxmysql
|
||||
mkdir -p ./temp/oxmysql/web/
|
||||
mkdir -p ./temp/oxmysql/lib/
|
||||
cp ./{LICENSE,README.md,fxmanifest.lua,ui.lua} ./temp/oxmysql
|
||||
cp ./lib/MySQL.lua ./temp/oxmysql/lib
|
||||
cp -r ./dist ./temp/oxmysql
|
||||
cp -r ./web/build ./temp/oxmysql/web/
|
||||
cd ./temp && zip -r ../oxmysql.zip ./oxmysql
|
||||
|
||||
- name: Create Release
|
||||
uses: 'marvinpinto/action-automatic-releases@v1.2.1'
|
||||
id: auto_release
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: 'Experimental - ${{ github.event.inputs.tag }} (FOR TESTING PURPOSE)'
|
||||
prerelease: true
|
||||
automatic_release_tag: '${{ github.event.inputs.tag }}'
|
||||
files: oxmysql-${{ github.event.inputs.tag }}.zip
|
||||
|
||||
env:
|
||||
CI: false
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Build and Create Tagged release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install archive tools
|
||||
run: sudo apt install zip
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get variables
|
||||
id: get_vars
|
||||
run: |
|
||||
echo '::set-output name=SHORT_SHA::$(git rev-parse --short HEAD)'
|
||||
echo '::set-output name=DATE::$(date +'%D')'
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Run build
|
||||
run: pnpm build
|
||||
env:
|
||||
TGT_RELEASE_VERSION: ${{ github.ref_name }}
|
||||
|
||||
- name: Update repository
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -am '${{ github.ref_name }}'
|
||||
git push
|
||||
|
||||
- name: Bundle files
|
||||
run: |
|
||||
mkdir -p ./temp/oxmysql/{web,lib}
|
||||
cp ./{LICENSE,README.md,fxmanifest.lua,ui.lua} ./temp/oxmysql
|
||||
cp ./lib/MySQL.lua ./temp/oxmysql/lib
|
||||
cp -r ./dist ./logger ./temp/oxmysql
|
||||
cp -r ./web/build ./temp/oxmysql/web/
|
||||
cd ./temp && zip -r ../oxmysql.zip ./oxmysql
|
||||
|
||||
- name: Create Release
|
||||
uses: 'marvinpinto/action-automatic-releases@v1.2.1'
|
||||
id: auto_release
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: '${{ env.RELEASE_VERSION }}'
|
||||
prerelease: false
|
||||
files: oxmysql.zip
|
||||
|
||||
env:
|
||||
CI: false
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,129 @@
|
||||
.yarn.installed
|
||||
fxmanifest.lua
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# JetBrains
|
||||
.idea
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Wrapper
|
||||
lib/MySQL.js
|
||||
lib/MySQL.d.ts
|
||||
lib/MySQL.js.map
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,34 @@
|
||||
# oxmysql
|
||||
|
||||
A FiveM resource to communicate with a MySQL database using [node-mysql2](https://github.com/sidorares/node-mysql2).
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🔗 Links
|
||||
- 💾 [Download](https://github.com/overextended/oxmysql/releases/latest/download/oxmysql.zip)
|
||||
- Download the latest release directly.
|
||||
- 📚 [Documentation](https://overextended.dev/oxmysql)
|
||||
- For installation, setup, and everything else.
|
||||
- 📦 [npm](https://www.npmjs.com/package/@overextended/oxmysql)
|
||||
- Use our npm package for enhanced functionality and TypeScript support.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Support for mysql-async and ghmattimysql syntax.
|
||||
- Promises / async query handling allowing for non-blocking and awaitable responses.
|
||||
- Improved performance and stability compared to other options.
|
||||
- Support for named and unnamed placeholders, improving performance and security.
|
||||
- Support for URI connection strings and semicolon separated values.
|
||||
- Improved parameter checking and error handling.
|
||||
|
||||
## 🧾 Logging
|
||||
|
||||
We have included a module for submitting error logs to [Fivemanage](https://fivemanage.com/?ref=overextended), a cloud management service tailored for game servers. Additional logging options and support for other services will be available in the future.
|
||||
|
||||
## Lua Language Server
|
||||
|
||||
- Install [Lua Language Server](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) to ease development with annotations, type checking, diagnostics, and more.
|
||||
- See [ox_types](https://github.com/overextended/ox_types) for our Lua type definitions.
|
||||
@@ -0,0 +1,74 @@
|
||||
import { build } from 'esbuild';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('package.json', { encoding: 'utf8' }));
|
||||
const version = process.env.TGT_RELEASE_VERSION;
|
||||
|
||||
if (version) {
|
||||
packageJson.version = version.replace('v', '');
|
||||
writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
'.yarn.installed',
|
||||
new Date().toLocaleString('en-AU', {
|
||||
timeZone: 'UTC',
|
||||
timeStyle: 'long',
|
||||
dateStyle: 'full',
|
||||
})
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
'fxmanifest.lua',
|
||||
`fx_version 'cerulean'
|
||||
game 'common'
|
||||
use_experimental_fxv2_oal 'yes'
|
||||
lua54 'yes'
|
||||
node_version '22'
|
||||
|
||||
name '${packageJson.name}'
|
||||
author '${packageJson.author}'
|
||||
version '${packageJson.version}'
|
||||
license '${packageJson.license}'
|
||||
repository '${packageJson.repository.url}'
|
||||
description '${packageJson.description}'
|
||||
|
||||
dependencies {
|
||||
'/server:12913',
|
||||
}
|
||||
|
||||
client_script 'ui.lua'
|
||||
server_script 'dist/build.js'
|
||||
|
||||
files {
|
||||
'web/build/index.html',
|
||||
'web/build/**/*'
|
||||
}
|
||||
|
||||
ui_page 'web/build/index.html'
|
||||
|
||||
provide 'mysql-async'
|
||||
provide 'ghmattimysql'
|
||||
|
||||
convar_category 'OxMySQL' {
|
||||
'Configuration',
|
||||
{
|
||||
{ 'Connection string', 'mysql_connection_string', 'CV_STRING', 'mysql://user:password@localhost/database' },
|
||||
{ 'Debug', 'mysql_debug', 'CV_BOOL', 'false' }
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
build({
|
||||
bundle: true,
|
||||
entryPoints: [`./src/index.ts`],
|
||||
outfile: `dist/build.js`,
|
||||
keepNames: true,
|
||||
dropLabels: ['DEV'],
|
||||
legalComments: 'inline',
|
||||
platform: 'node',
|
||||
target: ['node22'],
|
||||
format: 'cjs',
|
||||
logLevel: 'info',
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"packages": ["web"],
|
||||
"npmClient": "pnpm",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
define.lua
|
||||
MySQL.lua
|
||||
@@ -0,0 +1,155 @@
|
||||
local promise = promise
|
||||
local Await = Citizen.Await
|
||||
local resourceName = GetCurrentResourceName()
|
||||
local GetResourceState = GetResourceState
|
||||
|
||||
local options = {
|
||||
return_callback_errors = false
|
||||
}
|
||||
|
||||
for i = 1, GetNumResourceMetadata(resourceName, 'mysql_option') do
|
||||
local option = GetResourceMetadata(resourceName, 'mysql_option', i - 1)
|
||||
options[option] = true
|
||||
end
|
||||
|
||||
local function await(fn, query, parameters)
|
||||
local p = promise.new()
|
||||
|
||||
fn(nil, query, parameters, function(result, error)
|
||||
if error then
|
||||
return p:reject(error)
|
||||
end
|
||||
|
||||
p:resolve(result)
|
||||
end, resourceName, true)
|
||||
|
||||
return Await(p)
|
||||
end
|
||||
|
||||
local type = type
|
||||
local queryStore = {}
|
||||
|
||||
local function safeArgs(query, parameters, cb, transaction)
|
||||
local queryType = type(query)
|
||||
|
||||
if queryType == 'number' then
|
||||
query = queryStore[query]
|
||||
assert(query, "First argument received invalid query store reference")
|
||||
elseif transaction then
|
||||
if queryType ~= 'table' then
|
||||
error(("First argument expected table, received '%s'"):format(query))
|
||||
end
|
||||
elseif queryType ~= 'string' then
|
||||
error(("First argument expected string, received '%s'"):format(query))
|
||||
end
|
||||
|
||||
if parameters then
|
||||
local paramType = type(parameters)
|
||||
|
||||
if paramType ~= 'table' and paramType ~= 'function' then
|
||||
error(("Second argument expected table or function, received '%s'"):format(parameters))
|
||||
end
|
||||
|
||||
if paramType == 'function' or parameters.__cfx_functionReference then
|
||||
cb = parameters
|
||||
parameters = nil
|
||||
end
|
||||
end
|
||||
|
||||
if cb and parameters then
|
||||
local cbType = type(cb)
|
||||
|
||||
if cbType ~= 'function' and (cbType == 'table' and not cb.__cfx_functionReference) then
|
||||
error(("Third argument expected function, received '%s'"):format(cb))
|
||||
end
|
||||
end
|
||||
|
||||
return query, parameters, cb
|
||||
end
|
||||
|
||||
local oxmysql = exports.oxmysql
|
||||
|
||||
local mysql_method_mt = {
|
||||
__call = function(self, query, parameters, cb)
|
||||
query, parameters, cb = safeArgs(query, parameters, cb, self.method == 'transaction')
|
||||
return oxmysql[self.method](nil, query, parameters, cb, resourceName, options.return_callback_errors)
|
||||
end
|
||||
}
|
||||
|
||||
local MySQL = setmetatable(MySQL or {}, {
|
||||
__index = function(_, index)
|
||||
return function(...)
|
||||
return oxmysql[index](nil, ...)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
for _, method in pairs({
|
||||
'scalar', 'single', 'query', 'insert', 'update', 'prepare', 'transaction', 'rawExecute',
|
||||
}) do
|
||||
MySQL[method] = setmetatable({
|
||||
method = method,
|
||||
await = function(query, parameters)
|
||||
query, parameters = safeArgs(query, parameters, nil, method == 'transaction')
|
||||
return await(oxmysql[method], query, parameters)
|
||||
end
|
||||
}, mysql_method_mt)
|
||||
end
|
||||
|
||||
local alias = {
|
||||
fetchAll = 'query',
|
||||
fetchScalar = 'scalar',
|
||||
fetchSingle = 'single',
|
||||
insert = 'insert',
|
||||
execute = 'update',
|
||||
transaction = 'transaction',
|
||||
prepare = 'prepare'
|
||||
}
|
||||
|
||||
local alias_mt = {
|
||||
__index = function(self, key)
|
||||
if alias[key] then
|
||||
local method = MySQL[alias[key]]
|
||||
MySQL.Async[key] = method
|
||||
MySQL.Sync[key] = method.await
|
||||
alias[key] = nil
|
||||
return self[key]
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function addStore(query, cb)
|
||||
assert(type(query) == 'string', 'The SQL Query must be a string')
|
||||
|
||||
local storeN = #queryStore + 1
|
||||
queryStore[storeN] = query
|
||||
|
||||
return cb and cb(storeN) or storeN
|
||||
end
|
||||
|
||||
MySQL.Sync = setmetatable({ store = addStore }, alias_mt)
|
||||
MySQL.Async = setmetatable({ store = addStore }, alias_mt)
|
||||
|
||||
local function onReady(cb)
|
||||
while GetResourceState('oxmysql') ~= 'started' do
|
||||
Wait(50)
|
||||
end
|
||||
|
||||
oxmysql.awaitConnection()
|
||||
|
||||
return cb and cb() or true
|
||||
end
|
||||
|
||||
MySQL.ready = setmetatable({
|
||||
await = onReady
|
||||
}, {
|
||||
__call = function(_, cb)
|
||||
Citizen.CreateThreadNow(function() onReady(cb) end)
|
||||
end,
|
||||
})
|
||||
|
||||
function MySQL.startTransaction(cb)
|
||||
return oxmysql:startTransaction(cb, resourceName)
|
||||
end
|
||||
|
||||
_ENV.MySQL = MySQL
|
||||
@@ -0,0 +1,168 @@
|
||||
type Query = string | number;
|
||||
type Params = Record<string, unknown> | unknown[] | Function;
|
||||
type Callback<T> = (result: T | null) => void;
|
||||
|
||||
type Transaction =
|
||||
| string[]
|
||||
| [string, Params][]
|
||||
| { query: string; values: Params }[]
|
||||
| { query: string; parameters: Params }[];
|
||||
|
||||
interface Result {
|
||||
[column: string | number]: any;
|
||||
affectedRows?: number;
|
||||
fieldCount?: number;
|
||||
info?: string;
|
||||
insertId?: number;
|
||||
serverStatus?: number;
|
||||
warningStatus?: number;
|
||||
changedRows?: number;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
[column: string | number]: unknown;
|
||||
}
|
||||
|
||||
interface OxMySQL {
|
||||
store: (query: string) => void;
|
||||
ready: (callback: () => void) => void;
|
||||
query: <T = Result | null>(query: Query, params?: Params | Callback<T>, cb?: Callback<T>) => Promise<T>;
|
||||
single: <T = Row | null>(
|
||||
query: Query,
|
||||
params?: Params | Callback<Exclude<T, []>>,
|
||||
cb?: Callback<Exclude<T, []>>
|
||||
) => Promise<Exclude<T, []>>;
|
||||
scalar: <T = unknown | null>(
|
||||
query: Query,
|
||||
params?: Params | Callback<Exclude<T, []>>,
|
||||
cb?: Callback<Exclude<T, []>>
|
||||
) => Promise<Exclude<T, []>>;
|
||||
update: <T = number | null>(query: Query, params?: Params | Callback<T>, cb?: Callback<T>) => Promise<T>;
|
||||
insert: <T = number | null>(query: Query, params?: Params | Callback<T>, cb?: Callback<T>) => Promise<T>;
|
||||
prepare: <T = any>(query: Query, params?: Params | Callback<T>, cb?: Callback<T>) => Promise<T>;
|
||||
rawExecute: <T = Result | null>(query: Query, params?: Params | Callback<T>, cb?: Callback<T>) => Promise<T>;
|
||||
transaction: (query: Transaction, params?: Params | Callback<boolean>, cb?: Callback<boolean>) => Promise<boolean>;
|
||||
isReady: () => boolean;
|
||||
awaitConnection: () => Promise<true>;
|
||||
startTransaction: (
|
||||
cb: (query: <T = Result | null>(statement: string, params?: Params) => Promise<T>) => Promise<boolean | void>
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const QueryStore: string[] = [];
|
||||
|
||||
function assert(condition: boolean, message: string) {
|
||||
if (!condition) throw new TypeError(message);
|
||||
}
|
||||
|
||||
const safeArgs = (query: Query | Transaction, params?: any, cb?: Function, transaction?: true) => {
|
||||
if (typeof query === 'number') {
|
||||
query = QueryStore[query];
|
||||
assert(typeof query === 'string', 'First argument received invalid query store reference');
|
||||
}
|
||||
|
||||
if (transaction) {
|
||||
assert(typeof query === 'object', `First argument expected object, recieved ${typeof query}`);
|
||||
} else {
|
||||
assert(typeof query === 'string', `First argument expected string, received ${typeof query}`);
|
||||
}
|
||||
|
||||
if (params) {
|
||||
const paramType = typeof params;
|
||||
assert(
|
||||
paramType === 'object' || paramType === 'function',
|
||||
`Second argument expected object or function, received ${paramType}`
|
||||
);
|
||||
|
||||
if (!cb && paramType === 'function') {
|
||||
cb = params;
|
||||
params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (cb !== undefined) assert(typeof cb === 'function', `Third argument expected function, received ${typeof cb}`);
|
||||
|
||||
return [query, params, cb];
|
||||
};
|
||||
|
||||
declare var global: any;
|
||||
const exp = global.exports.oxmysql;
|
||||
const currentResourceName = GetCurrentResourceName();
|
||||
|
||||
function execute(method: string, query: Query | Transaction, params?: Params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exp[method](
|
||||
query,
|
||||
params,
|
||||
(result, error) => {
|
||||
if (error) return reject(error);
|
||||
resolve(result);
|
||||
},
|
||||
currentResourceName,
|
||||
true
|
||||
);
|
||||
}) as any;
|
||||
}
|
||||
|
||||
export const oxmysql: OxMySQL = {
|
||||
store(query) {
|
||||
assert(typeof query !== 'string', `Query expects a string, received ${typeof query}`);
|
||||
|
||||
return QueryStore.push(query);
|
||||
},
|
||||
ready(callback) {
|
||||
setImmediate(async () => {
|
||||
while (GetResourceState('oxmysql') !== 'started') await new Promise((resolve) => setTimeout(resolve, 50, null));
|
||||
callback();
|
||||
});
|
||||
},
|
||||
async query(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('query', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async single(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('single', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async scalar(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('scalar', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async update(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('update', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async insert(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('insert', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async prepare(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('prepare', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async rawExecute(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb);
|
||||
const result = await execute('rawExecute', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
async transaction(query, params, cb) {
|
||||
[query, params, cb] = safeArgs(query, params, cb, true);
|
||||
const result = await execute('transaction', query, params);
|
||||
return cb ? cb(result) : result;
|
||||
},
|
||||
isReady() {
|
||||
return exp.isReady();
|
||||
},
|
||||
async awaitConnection() {
|
||||
return await exp.awaitConnection();
|
||||
},
|
||||
async startTransaction(cb) {
|
||||
return exp.startTransaction(cb, currentResourceName);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
# OxMySQL exports wrapper for FiveM
|
||||
|
||||
Types are fully supported and you will get intellisense on the `oxmysql` object when using it.
|
||||
|
||||
## Installation
|
||||
|
||||
```yaml
|
||||
# With pnpm
|
||||
pnpm add @overextended/oxmysql
|
||||
|
||||
# With Yarn
|
||||
yarn add @overextended/oxmysql
|
||||
|
||||
# With npm
|
||||
npm install @overextended/oxmysql
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Import as module:
|
||||
|
||||
```js
|
||||
import { oxmysql } from '@overextended/oxmysql';
|
||||
```
|
||||
|
||||
Import with require:
|
||||
|
||||
```js
|
||||
const { oxmysql } = require('@overextended/oxmysql');
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
[View documentation](https://overextended.github.io/docs/oxmysql)
|
||||
|
||||
```js
|
||||
oxmysql.scalar('SELECT username FROM users', (result) => {
|
||||
console.log(result)
|
||||
}).catch(console.error)
|
||||
|
||||
oxmysql.scalar('SELECT username FROM users').then((result) => {
|
||||
console.log(result)
|
||||
}).catch(console.error)
|
||||
|
||||
const result = await oxmysql.scalar('SELECT username FROM users').catch(console.error)
|
||||
console.log(result)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@overextended/oxmysql",
|
||||
"version": "1.4.2",
|
||||
"description": "Exports wrapper for oxmysql",
|
||||
"types": "MySQL.d.ts",
|
||||
"main": "MySQL.js",
|
||||
"scripts": {
|
||||
"prepublish": "tsc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/overextended/oxmysql.git"
|
||||
},
|
||||
"keywords": [
|
||||
"fivem",
|
||||
"oxmysql",
|
||||
"sql"
|
||||
],
|
||||
"author": "Overextended",
|
||||
"license": "LGPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/overextended/oxmysql/issues"
|
||||
},
|
||||
"homepage": "https://github.com/overextended/oxmysql#readme"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": ".",
|
||||
"strict": true,
|
||||
"module": "CommonJS",
|
||||
"types": ["@citizenfx/server"],
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"lib": ["ESNext"],
|
||||
"noEmitOnError": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["MySQL.ts"],
|
||||
"exclude": ["**/node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// https://fivemanage.com/?ref=overextended
|
||||
|
||||
const apiKey = GetConvar('FIVEMANAGE_LOGS_API_KEY', '');
|
||||
|
||||
if (!apiKey) return console.warning(`convar "FIVEMANAGE_LOGS_API_KEY" has not been set`);
|
||||
|
||||
const batchedLogs = [];
|
||||
const endpoint = 'https://api.fivemanage.com/api/logs/batch';
|
||||
const headers = {
|
||||
['Content-Type']: 'application/json',
|
||||
['Authorization']: apiKey,
|
||||
['User-Agent']: 'oxmysql',
|
||||
};
|
||||
|
||||
async function sendLogs() {
|
||||
try {
|
||||
const body = JSON.stringify(batchedLogs);
|
||||
batchedLogs.length = 0;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
if (response.ok) return;
|
||||
|
||||
console.error(`Failed to submit logs to fivemanage - ${response.status} ${response.statusText}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return function logger(data) {
|
||||
if (batchedLogs.length === 0) setTimeout(sendLogs, 500);
|
||||
|
||||
batchedLogs.push(data);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "oxmysql",
|
||||
"version": "2.13.0",
|
||||
"description": "FXServer to MySQL communication via node-mysql2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/overextended/oxmysql.git"
|
||||
},
|
||||
"bugs": "https://github.com/overextended/oxmysql/issues",
|
||||
"author": "Overextended",
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"main": "lib/MySQL.js",
|
||||
"types": "lib/MySQL.d.ts",
|
||||
"files": [
|
||||
"lib/MySQL.js",
|
||||
"lib/MySQL.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm build:root && lerna run build",
|
||||
"build:root": "node build.js && cd lib && tsc",
|
||||
"watch": "esbuild --watch --bundle --platform=node --target=node16.9.1 src/index.ts --outfile=dist/build.js",
|
||||
"lib": "tsc --project lib/tsconfig.lib.json",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"postinstall": "patch-package && pnpm bootstrap"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "3.11.3",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@citizenfx/server": "2.0.5132-1",
|
||||
"@milahu/patch-package": "^6.4.14",
|
||||
"@types/node": "^22.7.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"lerna": "^4.0.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
# generated by patch-package 6.4.14
|
||||
#
|
||||
# declared package:
|
||||
# mysql2: 3.11.3
|
||||
#
|
||||
diff --git a/node_modules/mysql2/lib/connection.js b/node_modules/mysql2/lib/connection.js
|
||||
index af6b3d9..a427374 100644
|
||||
--- a/node_modules/mysql2/lib/connection.js
|
||||
+++ b/node_modules/mysql2/lib/connection.js
|
||||
@@ -32,7 +32,7 @@ const CharsetToEncoding = require('./constants/charset_encodings.js');
|
||||
|
||||
let _connectionId = 0;
|
||||
|
||||
-let convertNamedPlaceholders = null;
|
||||
+// let convertNamedPlaceholders = null;
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
constructor(opts) {
|
||||
@@ -524,7 +524,7 @@ class Connection extends EventEmitter {
|
||||
sql: sql,
|
||||
values: values
|
||||
};
|
||||
- this._resolveNamedPlaceholders(opts);
|
||||
+ // this._resolveNamedPlaceholders(opts);
|
||||
return SqlString.format(
|
||||
opts.sql,
|
||||
opts.values,
|
||||
@@ -546,20 +546,20 @@ class Connection extends EventEmitter {
|
||||
}
|
||||
|
||||
_resolveNamedPlaceholders(options) {
|
||||
- let unnamed;
|
||||
- if (this.config.namedPlaceholders || options.namedPlaceholders) {
|
||||
- if (Array.isArray(options.values)) {
|
||||
- // if an array is provided as the values, assume the conversion is not necessary.
|
||||
- // this allows the usage of unnamed placeholders even if the namedPlaceholders flag is enabled.
|
||||
- return
|
||||
- }
|
||||
- if (convertNamedPlaceholders === null) {
|
||||
- convertNamedPlaceholders = require('named-placeholders')();
|
||||
- }
|
||||
- unnamed = convertNamedPlaceholders(options.sql, options.values);
|
||||
- options.sql = unnamed[0];
|
||||
- options.values = unnamed[1];
|
||||
- }
|
||||
+ // let unnamed;
|
||||
+ // if (this.config.namedPlaceholders || options.namedPlaceholders) {
|
||||
+ // if (Array.isArray(options.values)) {
|
||||
+ // // if an array is provided as the values, assume the conversion is not necessary.
|
||||
+ // // this allows the usage of unnamed placeholders even if the namedPlaceholders flag is enabled.
|
||||
+ // return
|
||||
+ // }
|
||||
+ // if (convertNamedPlaceholders === null) {
|
||||
+ // convertNamedPlaceholders = require('named-placeholders')();
|
||||
+ // }
|
||||
+ // unnamed = convertNamedPlaceholders(options.sql, options.values);
|
||||
+ // options.sql = unnamed[0];
|
||||
+ // options.values = unnamed[1];
|
||||
+ // }
|
||||
}
|
||||
|
||||
query(sql, values, cb) {
|
||||
@@ -569,7 +569,7 @@ class Connection extends EventEmitter {
|
||||
} else {
|
||||
cmdQuery = Connection.createQuery(sql, values, cb, this.config);
|
||||
}
|
||||
- this._resolveNamedPlaceholders(cmdQuery);
|
||||
+ // this._resolveNamedPlaceholders(cmdQuery);
|
||||
const rawSql = this.format(cmdQuery.sql, cmdQuery.values !== undefined ? cmdQuery.values : []);
|
||||
cmdQuery.sql = rawSql;
|
||||
return this.addCommand(cmdQuery);
|
||||
@@ -644,26 +644,27 @@ class Connection extends EventEmitter {
|
||||
options.sql = sql;
|
||||
options.values = values;
|
||||
}
|
||||
- this._resolveNamedPlaceholders(options);
|
||||
+ // this._resolveNamedPlaceholders(options);
|
||||
// check for values containing undefined
|
||||
if (options.values) {
|
||||
//If namedPlaceholder is not enabled and object is passed as bind parameters
|
||||
- if (!Array.isArray(options.values)) {
|
||||
- throw new TypeError(
|
||||
- 'Bind parameters must be array if namedPlaceholders parameter is not enabled'
|
||||
- );
|
||||
- }
|
||||
+ // if (!Array.isArray(options.values)) {
|
||||
+ // throw new TypeError(
|
||||
+ // 'Bind parameters must be array if namedPlaceholders parameter is not enabled'
|
||||
+ // );
|
||||
+ // }
|
||||
options.values.forEach(val => {
|
||||
//If namedPlaceholder is not enabled and object is passed as bind parameters
|
||||
- if (!Array.isArray(options.values)) {
|
||||
- throw new TypeError(
|
||||
- 'Bind parameters must be array if namedPlaceholders parameter is not enabled'
|
||||
- );
|
||||
- }
|
||||
+ // if (!Array.isArray(options.values)) {
|
||||
+ // throw new TypeError(
|
||||
+ // 'Bind parameters must be array if namedPlaceholders parameter is not enabled'
|
||||
+ // );
|
||||
+ // }
|
||||
if (val === undefined) {
|
||||
- throw new TypeError(
|
||||
- 'Bind parameters must not contain undefined. To pass SQL NULL specify JS null'
|
||||
- );
|
||||
+ // throw new TypeError(
|
||||
+ // 'Bind parameters must not contain undefined. To pass SQL NULL specify JS null'
|
||||
+ // );
|
||||
+ val = null
|
||||
}
|
||||
if (typeof val === 'function') {
|
||||
throw new TypeError(
|
||||
diff --git a/node_modules/mysql2/lib/packets/execute.js b/node_modules/mysql2/lib/packets/execute.js
|
||||
index daf1df9..0862ce1 100644
|
||||
--- a/node_modules/mysql2/lib/packets/execute.js
|
||||
+++ b/node_modules/mysql2/lib/packets/execute.js
|
||||
@@ -28,7 +28,10 @@ function toParameter(value, encoding, timezone) {
|
||||
if (value !== null) {
|
||||
switch (typeof value) {
|
||||
case 'undefined':
|
||||
- throw new TypeError('Bind parameters must not contain undefined');
|
||||
+ // throw new TypeError('Bind parameters must not contain undefined');
|
||||
+ value = '';
|
||||
+ type = Types.NULL;
|
||||
+ break;
|
||||
|
||||
case 'number':
|
||||
type = Types.DOUBLE;
|
||||
diff --git a/node_modules/mysql2/lib/parsers/binary_parser.js b/node_modules/mysql2/lib/parsers/binary_parser.js
|
||||
index 4eb0d3a..bf6dbec 100644
|
||||
--- a/node_modules/mysql2/lib/parsers/binary_parser.js
|
||||
+++ b/node_modules/mysql2/lib/parsers/binary_parser.js
|
||||
@@ -77,7 +77,7 @@ function readCodeFor(field, config, options, fieldNum) {
|
||||
|
||||
default:
|
||||
if (field.characterSet === Charsets.BINARY) {
|
||||
- return 'packet.readLengthCodedBuffer();';
|
||||
+ return '[...packet.readLengthCodedBuffer()];';
|
||||
}
|
||||
return `packet.readLengthCodedString(fields[${fieldNum}].encoding)`;
|
||||
}
|
||||
@@ -94,6 +94,7 @@ function compile(fields, options, config) {
|
||||
db: field.schema,
|
||||
table: field.table,
|
||||
name: field.name,
|
||||
+ charset: field.characterSet,
|
||||
string: function (encoding = field.encoding) {
|
||||
if (field.columnType === Types.JSON && encoding === field.encoding) {
|
||||
// Since for JSON columns mysql always returns charset 63 (BINARY),
|
||||
diff --git a/node_modules/mysql2/lib/parsers/text_parser.js b/node_modules/mysql2/lib/parsers/text_parser.js
|
||||
index deedadf..7a4a6c8 100644
|
||||
--- a/node_modules/mysql2/lib/parsers/text_parser.js
|
||||
+++ b/node_modules/mysql2/lib/parsers/text_parser.js
|
||||
@@ -90,6 +90,7 @@ function compile(fields, options, config) {
|
||||
db: field.schema,
|
||||
table: field.table,
|
||||
name: field.name,
|
||||
+ charset: field.characterSet,
|
||||
string: function (encoding = field.encoding) {
|
||||
if (field.columnType === Types.JSON && encoding === field.encoding) {
|
||||
// Since for JSON columns mysql always returns charset 63 (BINARY),
|
||||
diff --git a/node_modules/mysql2/typings/mysql/lib/parsers/typeCast.d.ts b/node_modules/mysql2/typings/mysql/lib/parsers/typeCast.d.ts
|
||||
index b8e9751..ca8fbda 100644
|
||||
--- a/node_modules/mysql2/typings/mysql/lib/parsers/typeCast.d.ts
|
||||
+++ b/node_modules/mysql2/typings/mysql/lib/parsers/typeCast.d.ts
|
||||
@@ -44,6 +44,7 @@ export type Field = Type & {
|
||||
db: string;
|
||||
table: string;
|
||||
name: string;
|
||||
+ charset: number;
|
||||
string: (encoding?: BufferEncoding | string | undefined) => string | null;
|
||||
buffer: () => Buffer | null;
|
||||
geometry: () => Geometry | Geometry[] | null;
|
||||
@@ -0,0 +1,65 @@
|
||||
# generated by patch-package 6.4.14
|
||||
#
|
||||
# declared package:
|
||||
# named-placeholders: ^1.1.3
|
||||
#
|
||||
diff --git a/node_modules/named-placeholders/index.js b/node_modules/named-placeholders/index.js
|
||||
index 3524ef5..e47335e 100644
|
||||
--- a/node_modules/named-placeholders/index.js
|
||||
+++ b/node_modules/named-placeholders/index.js
|
||||
@@ -3,7 +3,7 @@
|
||||
// based on code from Brian White @mscdex mariasql library - https://github.com/mscdex/node-mariasql/blob/master/lib/Client.js#L272-L332
|
||||
// License: https://github.com/mscdex/node-mariasql/blob/master/LICENSE
|
||||
|
||||
-const RE_PARAM = /(?:\?)|(?::(\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g,
|
||||
+const RE_PARAM = /(?:\?)|(?:(?<!["'])[:@](\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g,
|
||||
DQUOTE = 34,
|
||||
SQUOTE = 39,
|
||||
BSLASH = 92;
|
||||
@@ -92,15 +92,24 @@ function createCompiler(config) {
|
||||
if (typeof params == 'undefined')
|
||||
throw new Error('Named query contains placeholders, but parameters object is undefined');
|
||||
|
||||
+ for(const key in params) {
|
||||
+ const char = key[0]
|
||||
+ if(char == '@' || char == ':') {
|
||||
+ params[key.substring(1)] = params[key];
|
||||
+ delete params[key];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
const tokens = tree[1];
|
||||
for (let i=0; i < tokens.length; ++i) {
|
||||
- arr.push(params[tokens[i]]);
|
||||
+ arr.push(params[tokens[i]] === undefined ? null : params[tokens[i]]);
|
||||
}
|
||||
return [tree[0], arr];
|
||||
}
|
||||
|
||||
function noTailingSemicolon(s) {
|
||||
- if (s.slice(-1) == ':') {
|
||||
+ const char = s.slice(-1)
|
||||
+ if (char == ':' || char == '@') {
|
||||
return s.slice(0, -1);
|
||||
}
|
||||
return s;
|
||||
@@ -113,7 +122,8 @@ function createCompiler(config) {
|
||||
|
||||
let unnamed = noTailingSemicolon(tree[0][0]);
|
||||
for (let i=1; i < tree[0].length; ++i) {
|
||||
- if (tree[0][i-1].slice(-1) == ':') {
|
||||
+ const char = tree[0][i-1].slice(-1)
|
||||
+ if (char == ':' || char == '@') {
|
||||
unnamed += config.placeholder;
|
||||
}
|
||||
unnamed += config.placeholder;
|
||||
@@ -122,7 +132,8 @@ function createCompiler(config) {
|
||||
|
||||
const last = tree[0][tree[0].length -1];
|
||||
if (tree[0].length == tree[1].length) {
|
||||
- if (last.slice(-1) == ':') {
|
||||
+ const char = last.slice(-1)
|
||||
+ if (char == ':' || char == '@') {
|
||||
unnamed += config.placeholder;
|
||||
}
|
||||
unnamed += config.placeholder;
|
||||
+6114
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
query: 'execute',
|
||||
scalar: 'scalar',
|
||||
transaction: 'transaction',
|
||||
store: 'store',
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
update: 'mysql_execute',
|
||||
insert: 'mysql_insert',
|
||||
query: 'mysql_fetch_all',
|
||||
scalar: 'mysql_fetch_scalar',
|
||||
transaction: 'mysql_transaction',
|
||||
store: 'mysql_store',
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { ConnectionOptions } from 'mysql2';
|
||||
import { typeCast } from './utils/typeCast';
|
||||
|
||||
export const mysql_connection_string = GetConvar('mysql_connection_string', '');
|
||||
export let mysql_ui = GetConvar('mysql_ui', 'false') === 'true';
|
||||
export let mysql_slow_query_warning = GetConvarInt('mysql_slow_query_warning', 200);
|
||||
export let mysql_debug: boolean | string[] = false;
|
||||
|
||||
// max array size of individual resource query logs
|
||||
// prevent excessive memory use when people use debug/ui in production
|
||||
export let mysql_log_size = 0;
|
||||
|
||||
export function setDebug() {
|
||||
mysql_ui = GetConvar('mysql_ui', 'false') === 'true';
|
||||
mysql_slow_query_warning = GetConvarInt('mysql_slow_query_warning', 200);
|
||||
|
||||
try {
|
||||
const debug = GetConvar('mysql_debug', 'false');
|
||||
mysql_debug = debug === 'false' ? false : JSON.parse(debug);
|
||||
} catch (e) {
|
||||
mysql_debug = true;
|
||||
}
|
||||
|
||||
mysql_log_size = mysql_debug ? 10000 : GetConvarInt('mysql_log_size', 100);
|
||||
}
|
||||
|
||||
export const mysql_transaction_isolation_level = (() => {
|
||||
const query = 'SET TRANSACTION ISOLATION LEVEL';
|
||||
switch (GetConvarInt('mysql_transaction_isolation_level', 2)) {
|
||||
case 1:
|
||||
return `${query} REPEATABLE READ`;
|
||||
case 2:
|
||||
return `${query} READ COMMITTED`;
|
||||
case 3:
|
||||
return `${query} READ UNCOMMITTED`;
|
||||
case 4:
|
||||
return `${query} SERIALIZABLE`;
|
||||
default:
|
||||
return `${query} READ COMMITTED`;
|
||||
}
|
||||
})();
|
||||
|
||||
function parseUri(connectionString: string) {
|
||||
const splitMatchGroups = connectionString.match(
|
||||
new RegExp(
|
||||
'^(?:([^:/?#.]+):)?(?://(?:([^/?#]*)@)?([\\w\\d\\-\\u0100-\\uffff.%]*)(?::([0-9]+))?)?([^?#]+)?(?:\\?([^#]*))?$'
|
||||
)
|
||||
) as RegExpMatchArray;
|
||||
|
||||
if (!splitMatchGroups) throw new Error(`mysql_connection_string structure was invalid (${connectionString})`);
|
||||
|
||||
const authTarget = splitMatchGroups[2] ? splitMatchGroups[2].split(':') : [];
|
||||
|
||||
const options = {
|
||||
user: authTarget[0] || undefined,
|
||||
password: authTarget[1] || undefined,
|
||||
host: splitMatchGroups[3],
|
||||
port: parseInt(splitMatchGroups[4]),
|
||||
database: splitMatchGroups[5]?.replace(/^\/+/, ''),
|
||||
...(splitMatchGroups[6] &&
|
||||
splitMatchGroups[6].split('&').reduce<Record<string, string>>((connectionInfo, parameter) => {
|
||||
const [key, value] = parameter.split('=');
|
||||
connectionInfo[key] = value;
|
||||
return connectionInfo;
|
||||
}, {})),
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export let convertNamedPlaceholders: null | ((query: string, parameters: Record<string, any>) => [string, any[]]);
|
||||
|
||||
export function getConnectionOptions(): ConnectionOptions {
|
||||
const options: Record<string, any> = mysql_connection_string.includes('mysql://')
|
||||
? parseUri(mysql_connection_string)
|
||||
: mysql_connection_string
|
||||
.replace(/(?:host(?:name)|ip|server|data\s?source|addr(?:ess)?)=/gi, 'host=')
|
||||
.replace(/(?:user\s?(?:id|name)?|uid)=/gi, 'user=')
|
||||
.replace(/(?:pwd|pass)=/gi, 'password=')
|
||||
.replace(/(?:db)=/gi, 'database=')
|
||||
.split(';')
|
||||
.reduce<Record<string, string>>((connectionInfo, parameter) => {
|
||||
const [key, value] = parameter.split('=');
|
||||
if (key) connectionInfo[key] = value;
|
||||
return connectionInfo;
|
||||
}, {});
|
||||
|
||||
convertNamedPlaceholders = options.namedPlaceholders === 'false' ? null : require('named-placeholders')();
|
||||
|
||||
for (const key of ['dateStrings', 'flags', 'ssl']) {
|
||||
const value = options[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
options[key] = JSON.parse(value);
|
||||
} catch (err) {
|
||||
console.log(`^3Failed to parse property ${key} in configuration (${err})!^0`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flags: string[] = options.flags || [];
|
||||
flags.push(options.database ? 'CONNECT_WITH_DB' : '-CONNECT_WITH_DB');
|
||||
|
||||
return {
|
||||
connectTimeout: 60000,
|
||||
trace: false,
|
||||
supportBigNumbers: true,
|
||||
jsonStrings: true,
|
||||
...options,
|
||||
typeCast,
|
||||
namedPlaceholders: false, // we use our own named-placeholders patch, disable mysql2s
|
||||
flags: flags,
|
||||
};
|
||||
}
|
||||
|
||||
RegisterCommand(
|
||||
'oxmysql_debug',
|
||||
(source: number, args: string[]) => {
|
||||
if (source !== 0) return console.log('^3This command can only be run server side^0');
|
||||
switch (args[0]) {
|
||||
case 'add':
|
||||
if (!Array.isArray(mysql_debug)) mysql_debug = [];
|
||||
mysql_debug.push(args[1]);
|
||||
SetConvar('mysql_debug', JSON.stringify(mysql_debug));
|
||||
return console.log(`^3Added ${args[1]} to mysql_debug^0`);
|
||||
|
||||
case 'remove':
|
||||
if (Array.isArray(mysql_debug)) {
|
||||
const index = mysql_debug.indexOf(args[1]);
|
||||
if (index === -1) return;
|
||||
mysql_debug.splice(index, 1);
|
||||
if (mysql_debug.length === 0) mysql_debug = false;
|
||||
SetConvar('mysql_debug', JSON.stringify(mysql_debug) || 'false');
|
||||
return console.log(`^3Removed ${args[1]} from mysql_debug^0`);
|
||||
}
|
||||
|
||||
default:
|
||||
return console.log(`^3Usage: oxmysql add|remove <resource>^0`);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { Connection, PoolConnection, TypeCast } from 'mysql2/promise';
|
||||
import { scheduleTick } from '../utils/scheduleTick';
|
||||
import { sleep } from '../utils/sleep';
|
||||
import { pool } from './pool';
|
||||
import type { CFXParameters } from 'types';
|
||||
import { typeCastExecute } from 'utils/typeCast';
|
||||
|
||||
(Symbol as any).dispose ??= Symbol('Symbol.dispose');
|
||||
|
||||
const activeConnections: Record<string, MySql> = {};
|
||||
|
||||
interface PromisePoolConnection extends Connection {
|
||||
connection: PoolConnection;
|
||||
release: PoolConnection['release'];
|
||||
}
|
||||
|
||||
export class MySql {
|
||||
id: number;
|
||||
connection: PromisePoolConnection;
|
||||
transaction?: boolean;
|
||||
|
||||
constructor(connection: PromisePoolConnection) {
|
||||
this.id = connection.connection.threadId;
|
||||
this.connection = connection;
|
||||
activeConnections[this.id] = this;
|
||||
}
|
||||
|
||||
async query(query: string, values: CFXParameters = []) {
|
||||
scheduleTick();
|
||||
|
||||
const [result] = await this.connection.query(query, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
async execute(query: string, values: CFXParameters = []) {
|
||||
scheduleTick();
|
||||
|
||||
const [result] = await this.connection.execute({
|
||||
sql: query,
|
||||
values: values,
|
||||
typeCast: typeCastExecute,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
beginTransaction() {
|
||||
this.transaction = true;
|
||||
return this.connection.beginTransaction();
|
||||
}
|
||||
|
||||
rollback() {
|
||||
delete this.transaction;
|
||||
return this.connection.rollback();
|
||||
}
|
||||
|
||||
commit() {
|
||||
delete this.transaction;
|
||||
return this.connection.commit();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
if (this.transaction) this.commit();
|
||||
|
||||
delete activeConnections[this.id];
|
||||
this.connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConnection(connectionId?: number) {
|
||||
while (!pool) await sleep(0);
|
||||
|
||||
return connectionId
|
||||
? activeConnections[connectionId]
|
||||
: new MySql((await pool.getConnection()) as unknown as PromisePoolConnection);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { setDebug } from '../config';
|
||||
import { sleep } from '../utils/sleep';
|
||||
import { pool, createConnectionPool } from './pool';
|
||||
|
||||
setTimeout(async () => {
|
||||
setDebug();
|
||||
|
||||
while (!pool) {
|
||||
await createConnectionPool();
|
||||
|
||||
if (!pool) await sleep(30000);
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
setDebug();
|
||||
}, 1000);
|
||||
|
||||
export * from './connection';
|
||||
export * from './rawQuery';
|
||||
export * from './rawExecute';
|
||||
export * from './rawTransaction';
|
||||
export * from './pool';
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getConnectionOptions, mysql_transaction_isolation_level } from 'config';
|
||||
import { createPool } from 'mysql2/promise';
|
||||
import type { Pool, RowDataPacket } from 'mysql2/promise';
|
||||
import { getConnection } from './connection';
|
||||
|
||||
export let pool: Pool;
|
||||
export let dbVersion = '';
|
||||
|
||||
export async function createConnectionPool() {
|
||||
const config = getConnectionOptions();
|
||||
|
||||
try {
|
||||
const dbPool = createPool(config);
|
||||
|
||||
dbPool.on('connection', (connection) => {
|
||||
connection.query(mysql_transaction_isolation_level);
|
||||
});
|
||||
|
||||
const [result] = (await dbPool.query('SELECT VERSION() as version')) as RowDataPacket[];
|
||||
dbVersion = `^5[${result[0].version}]`;
|
||||
|
||||
console.log(`${dbVersion} ^2Database server connection established!^0`);
|
||||
|
||||
if (config.multipleStatements) {
|
||||
console.warn(`multipleStatements is enabled. Used incorrectly, this option may cause SQL injection.`);
|
||||
}
|
||||
|
||||
pool = dbPool;
|
||||
} catch (err: any) {
|
||||
const message = err.message.includes('auth_gssapi_client')
|
||||
? `Requested authentication using unknown plugin auth_gssapi_client.`
|
||||
: err.message;
|
||||
|
||||
console.log(
|
||||
`^3Unable to establish a connection to the database (${err.code})!\n^1Error${
|
||||
err.errno ? ` ${err.errno}` : ''
|
||||
}: ${message}^0`
|
||||
);
|
||||
|
||||
console.log(`See https://github.com/overextended/oxmysql/issues/154 for more information.`);
|
||||
|
||||
if (config.password) config.password = '******';
|
||||
|
||||
console.log(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { logError, logQuery } from '../logger';
|
||||
import { CFXCallback, CFXParameters, QueryType } from '../types';
|
||||
import { parseResponse } from '../utils/parseResponse';
|
||||
import { executeType, parseExecute } from '../utils/parseExecute';
|
||||
import { getConnection } from './connection';
|
||||
import { setCallback } from '../utils/setCallback';
|
||||
import { performance } from 'perf_hooks';
|
||||
import validateResultSet from 'utils/validateResultSet';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import { profileBatchStatements, runProfiler } from 'profiler';
|
||||
|
||||
export const rawExecute = async (
|
||||
invokingResource: string,
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb?: CFXCallback,
|
||||
isPromise?: boolean,
|
||||
unpack?: boolean,
|
||||
connectionId?: number
|
||||
) => {
|
||||
cb = setCallback(parameters, cb);
|
||||
|
||||
let type: QueryType;
|
||||
let placeholders: number;
|
||||
|
||||
try {
|
||||
type = executeType(query);
|
||||
placeholders = query.split('?').length - 1;
|
||||
parameters = parseExecute(placeholders, parameters);
|
||||
} catch (err: any) {
|
||||
return logError(invokingResource, cb, isPromise, err, query, parameters);
|
||||
}
|
||||
|
||||
using connection = await getConnection(connectionId);
|
||||
|
||||
if (!connection) return;
|
||||
|
||||
try {
|
||||
const hasProfiler = await runProfiler(connection, invokingResource);
|
||||
const parametersLength = parameters.length == 0 ? 1 : parameters.length;
|
||||
const response = [] as any[];
|
||||
|
||||
for (let index = 0; index < parametersLength; index++) {
|
||||
const values = parameters[index];
|
||||
|
||||
if (values && placeholders > values.length) {
|
||||
for (let i = values.length; i < placeholders; i++) {
|
||||
values[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = !hasProfiler && performance.now();
|
||||
const result = await connection.execute(query, values);
|
||||
|
||||
if (Array.isArray(result) && result.length > 1) {
|
||||
for (const value of result) {
|
||||
response.push(unpack ? parseResponse(type, value as RowDataPacket[]) : value);
|
||||
}
|
||||
} else response.push(unpack ? parseResponse(type, result) : result);
|
||||
|
||||
if (hasProfiler && ((index > 0 && index % 100 === 0) || index === parametersLength - 1)) {
|
||||
await profileBatchStatements(connection, invokingResource, query, parameters, index < 100 ? 0 : index);
|
||||
} else if (startTime) {
|
||||
logQuery(invokingResource, query, performance.now() - startTime, values);
|
||||
}
|
||||
|
||||
validateResultSet(invokingResource, query, result);
|
||||
}
|
||||
|
||||
if (!cb) return response.length === 1 ? response[0] : response;
|
||||
|
||||
try {
|
||||
if (response.length === 1) {
|
||||
if (unpack && type === null) {
|
||||
if (response[0][0] && Object.keys(response[0][0]).length === 1) {
|
||||
cb(Object.values(response[0][0])[0]);
|
||||
} else cb(response[0][0]);
|
||||
} else {
|
||||
cb(response[0]);
|
||||
}
|
||||
} else {
|
||||
cb(response);
|
||||
}
|
||||
} catch (err) {
|
||||
if (typeof err === 'string') {
|
||||
if (err.includes('SCRIPT ERROR:')) return console.log(err);
|
||||
console.log(`^1SCRIPT ERROR in invoking resource ${invokingResource}: ${err}^0`);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
logError(invokingResource, cb, isPromise, err, query, parameters);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { parseArguments } from '../utils/parseArguments';
|
||||
import { setCallback } from '../utils/setCallback';
|
||||
import { parseResponse } from '../utils/parseResponse';
|
||||
import { logQuery, logError } from '../logger';
|
||||
import type { CFXCallback, CFXParameters } from '../types';
|
||||
import type { QueryType } from '../types';
|
||||
import { getConnection } from './connection';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import { performance } from 'perf_hooks';
|
||||
import validateResultSet from 'utils/validateResultSet';
|
||||
import { runProfiler } from 'profiler';
|
||||
|
||||
export const rawQuery = async (
|
||||
type: QueryType,
|
||||
invokingResource: string,
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb?: CFXCallback,
|
||||
isPromise?: boolean,
|
||||
connectionId?: number
|
||||
) => {
|
||||
cb = setCallback(parameters, cb);
|
||||
try {
|
||||
[query, parameters] = parseArguments(query, parameters);
|
||||
} catch (err: any) {
|
||||
return logError(invokingResource, cb, isPromise, err, query, parameters);
|
||||
}
|
||||
|
||||
using connection = await getConnection(connectionId);
|
||||
|
||||
if (!connection) return;
|
||||
|
||||
try {
|
||||
const hasProfiler = await runProfiler(connection, invokingResource);
|
||||
const startTime = !hasProfiler && performance.now();
|
||||
const result = await connection.query(query, parameters);
|
||||
|
||||
if (hasProfiler) {
|
||||
const profiler = <RowDataPacket[]>(
|
||||
await connection.query('SELECT FORMAT(SUM(DURATION) * 1000, 4) AS `duration` FROM INFORMATION_SCHEMA.PROFILING')
|
||||
);
|
||||
|
||||
if (profiler[0]) logQuery(invokingResource, query, parseFloat(profiler[0].duration), parameters);
|
||||
} else if (startTime) {
|
||||
logQuery(invokingResource, query, performance.now() - startTime, parameters);
|
||||
}
|
||||
|
||||
validateResultSet(invokingResource, query, result);
|
||||
|
||||
if (!cb) return parseResponse(type, result);
|
||||
|
||||
try {
|
||||
cb(parseResponse(type, result));
|
||||
} catch (err) {
|
||||
if (typeof err === 'string') {
|
||||
if (err.includes('SCRIPT ERROR:')) return console.log(err);
|
||||
console.log(`^1SCRIPT ERROR in invoking resource ${invokingResource}: ${err}^0`);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
logError(invokingResource, cb, isPromise, err, query, parameters, true);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { getConnection } from './connection';
|
||||
import { logError, logger, logQuery } from '../logger';
|
||||
import { CFXCallback, CFXParameters, TransactionQuery } from '../types';
|
||||
import { parseTransaction } from '../utils/parseTransaction';
|
||||
import { setCallback } from '../utils/setCallback';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { profileBatchStatements, runProfiler } from 'profiler';
|
||||
|
||||
const transactionError = (queries: { query: string; params?: CFXParameters }[], parameters: CFXParameters) => {
|
||||
`${queries.map((query) => `${query.query} ${JSON.stringify(query.params || [])}`).join('\n')}\n${JSON.stringify(
|
||||
parameters
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const rawTransaction = async (
|
||||
invokingResource: string,
|
||||
queries: TransactionQuery,
|
||||
parameters: CFXParameters,
|
||||
cb?: CFXCallback,
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
let transactions;
|
||||
cb = setCallback(parameters, cb);
|
||||
|
||||
try {
|
||||
transactions = parseTransaction(queries, parameters);
|
||||
} catch (err: any) {
|
||||
return logError(invokingResource, cb, isPromise, err);
|
||||
}
|
||||
|
||||
using connection = await getConnection();
|
||||
|
||||
if (!connection) return;
|
||||
|
||||
let response = false;
|
||||
|
||||
try {
|
||||
const hasProfiler = await runProfiler(connection, invokingResource);
|
||||
await connection.beginTransaction();
|
||||
const transactionsLength = transactions.length;
|
||||
|
||||
for (let i = 0; i < transactionsLength; i++) {
|
||||
const transaction = transactions[i];
|
||||
const startTime = !hasProfiler && performance.now();
|
||||
await connection.query(transaction.query, transaction.params);
|
||||
|
||||
if (hasProfiler && ((i > 0 && i % 100 === 0) || i === transactionsLength - 1)) {
|
||||
await profileBatchStatements(connection, invokingResource, transactions, null, i < 100 ? 0 : i);
|
||||
} else if (startTime) {
|
||||
logQuery(invokingResource, transaction.query, performance.now() - startTime, transaction.params);
|
||||
}
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
|
||||
response = true;
|
||||
} catch (err: any) {
|
||||
await connection.rollback().catch(() => {});
|
||||
|
||||
const transactionErrorMessage = err.sql || transactionError(transactions, parameters);
|
||||
const msg = `${invokingResource} was unable to complete a transaction!\n${transactionErrorMessage}\n${err.message}`;
|
||||
|
||||
console.error(msg);
|
||||
|
||||
TriggerEvent('oxmysql:transaction-error', {
|
||||
query: transactionErrorMessage,
|
||||
parameters: parameters,
|
||||
message: err.message,
|
||||
err: err,
|
||||
resource: invokingResource,
|
||||
});
|
||||
|
||||
if (typeof err === 'object' && err.message) delete err.sqlMessage;
|
||||
|
||||
logger({
|
||||
level: 'error',
|
||||
resource: invokingResource,
|
||||
message: msg,
|
||||
metadata: err,
|
||||
});
|
||||
}
|
||||
|
||||
if (cb)
|
||||
try {
|
||||
cb(response);
|
||||
} catch (err) {
|
||||
if (typeof err === 'string') {
|
||||
if (err.includes('SCRIPT ERROR:')) return console.log(err);
|
||||
console.log(`^1SCRIPT ERROR in invoking resource ${invokingResource}: ${err}^0`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MySql, getConnection } from './connection';
|
||||
import { logError } from '../logger';
|
||||
import { CFXCallback, CFXParameters } from '../types';
|
||||
import { parseArguments } from 'utils/parseArguments';
|
||||
|
||||
async function runQuery(conn: MySql | null, sql: string, values: CFXParameters) {
|
||||
[sql, values] = parseArguments(sql, values);
|
||||
|
||||
try {
|
||||
if (!conn) throw new Error(`Connection used by transaction timed out after 30 seconds.`);
|
||||
|
||||
return await conn.query(sql, values);
|
||||
} catch (err: any) {
|
||||
throw new Error(`Query: ${sql}\n${JSON.stringify(values)}\n${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const startTransaction = async (
|
||||
invokingResource: string,
|
||||
queries: (...args: any[]) => Promise<boolean>,
|
||||
cb?: CFXCallback,
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
using conn: MySql = await getConnection();
|
||||
let response: boolean | null = false;
|
||||
let closed = false;
|
||||
|
||||
if (!conn) return;
|
||||
|
||||
setTimeout(() => (closed = true), 30000);
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const commit = await queries((sql: string, values: CFXParameters) =>
|
||||
runQuery(closed ? null : conn, sql, values)
|
||||
);
|
||||
|
||||
if (closed) throw new Error(`Transaction has timed out after 30 seconds.`);
|
||||
|
||||
response = commit === false ? false : true;
|
||||
|
||||
if (!response) conn.rollback();
|
||||
} catch (err: any) {
|
||||
conn.rollback();
|
||||
logError(invokingResource, cb, isPromise, err);
|
||||
} finally {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
return cb ? cb(response) : response;
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { CFXCallback, CFXParameters, TransactionQuery } from './types';
|
||||
import { rawQuery, rawExecute, rawTransaction, pool } from './database';
|
||||
import { startTransaction } from 'database/startTransaction';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import ghmatti from './compatibility/ghmattimysql';
|
||||
import mysqlAsync from './compatibility/mysql-async';
|
||||
import('./update');
|
||||
|
||||
const MySQL = {} as Record<string, Function>;
|
||||
|
||||
MySQL.isReady = () => {
|
||||
return pool ? true : false;
|
||||
};
|
||||
|
||||
MySQL.awaitConnection = async () => {
|
||||
while (!pool) await sleep(0);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
MySQL.query = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawQuery(null, invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.single = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawQuery('single', invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.scalar = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawQuery('scalar', invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.update = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawQuery('update', invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.insert = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawQuery('insert', invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.transaction = (
|
||||
queries: TransactionQuery,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawTransaction(invokingResource, queries, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
MySQL.startTransaction = (
|
||||
transactions: () => Promise<boolean>,
|
||||
invokingResource = GetInvokingResource()
|
||||
) => {
|
||||
console.warn(`MySQL.startTransaction is "experimental" and may receive breaking changes.`)
|
||||
return startTransaction(invokingResource, transactions, undefined, true);
|
||||
};
|
||||
|
||||
MySQL.prepare = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawExecute(invokingResource, query, parameters, cb, isPromise, true);
|
||||
};
|
||||
|
||||
MySQL.rawExecute = (
|
||||
query: string,
|
||||
parameters: CFXParameters,
|
||||
cb: CFXCallback,
|
||||
invokingResource = GetInvokingResource(),
|
||||
isPromise?: boolean
|
||||
) => {
|
||||
rawExecute(invokingResource, query, parameters, cb, isPromise);
|
||||
};
|
||||
|
||||
// provide the store export for compatibility (ghmatti/mysql-async); simply returns the query as-is
|
||||
MySQL.store = (query: string, cb: Function) => {
|
||||
cb(query);
|
||||
};
|
||||
|
||||
// deprecated export names
|
||||
MySQL.execute = MySQL.query;
|
||||
MySQL.fetch = MySQL.query;
|
||||
|
||||
function provide(resourceName: string, method: string, cb: Function) {
|
||||
on(`__cfx_export_${resourceName}_${method}`, (setCb: Function) => setCb(cb));
|
||||
}
|
||||
|
||||
for (const key in MySQL) {
|
||||
const exp = MySQL[key];
|
||||
|
||||
const async_exp = (query: string, parameters: CFXParameters, invokingResource = GetInvokingResource()) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
MySQL[key](
|
||||
query,
|
||||
parameters,
|
||||
(result: unknown, err: string) => {
|
||||
if (err) return reject(new Error(err));
|
||||
resolve(result);
|
||||
},
|
||||
invokingResource,
|
||||
true
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
global.exports(key, exp);
|
||||
// async_retval
|
||||
global.exports(`${key}_async`, async_exp);
|
||||
// deprecated aliases for async_retval
|
||||
global.exports(`${key}Sync`, async_exp);
|
||||
|
||||
let alias = (ghmatti as any)[key];
|
||||
|
||||
if (alias) {
|
||||
provide('ghmattimysql', alias, exp);
|
||||
provide('ghmattimysql', `${alias}Sync`, async_exp);
|
||||
}
|
||||
|
||||
alias = (mysqlAsync as any)[key];
|
||||
|
||||
if (alias) {
|
||||
provide('mysql-async', alias, exp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { mysql_debug, mysql_log_size, mysql_slow_query_warning, mysql_ui } from '../config';
|
||||
import type { CFXCallback, CFXParameters } from '../types';
|
||||
import { dbVersion } from '../database';
|
||||
|
||||
let loggerResource = '';
|
||||
let loggerService = GetConvar('mysql_logger_service', '');
|
||||
|
||||
if (loggerService) {
|
||||
if (loggerService.startsWith('@')) {
|
||||
const [resource, ...path] = loggerService.slice(1).split('/');
|
||||
|
||||
if (resource && path) {
|
||||
loggerResource = resource;
|
||||
loggerService = path.join('/');
|
||||
}
|
||||
} else loggerService = `logger/${loggerService}`;
|
||||
}
|
||||
|
||||
export const logger =
|
||||
(loggerService &&
|
||||
new Function(LoadResourceFile(loggerResource || GetCurrentResourceName(), `${loggerService}.js`))()) ||
|
||||
(() => {});
|
||||
|
||||
export function logError(
|
||||
invokingResource: string,
|
||||
cb: CFXCallback | undefined,
|
||||
isPromise: boolean | undefined,
|
||||
err: any | string = '', // i cbf typing the error right now
|
||||
query?: string,
|
||||
parameters?: CFXParameters,
|
||||
includeParameters?: boolean
|
||||
) {
|
||||
const message = typeof err === 'object' ? err.message : err.replace(/SCRIPT ERROR: citizen:[\w\/\.]+:\d+[:\s]+/, '');
|
||||
|
||||
const output = `${invokingResource} was unable to execute a query!${query ? `\n${`Query: ${query}`}` : ''}${
|
||||
includeParameters ? `\n${JSON.stringify(parameters)}` : ''
|
||||
}\n${message}`;
|
||||
|
||||
TriggerEvent('oxmysql:error', {
|
||||
query: query,
|
||||
parameters: parameters,
|
||||
message: message,
|
||||
err: err,
|
||||
resource: invokingResource,
|
||||
});
|
||||
|
||||
if (typeof err === 'object' && err.message) delete err.sqlMessage;
|
||||
|
||||
logger({
|
||||
level: 'error',
|
||||
resource: invokingResource,
|
||||
message: message,
|
||||
metadata: err,
|
||||
});
|
||||
|
||||
if (cb && isPromise) {
|
||||
try {
|
||||
return cb(null, output);
|
||||
} catch (e) {}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(output);
|
||||
}
|
||||
|
||||
interface QueryData {
|
||||
date: number;
|
||||
query: string;
|
||||
executionTime: number;
|
||||
slow?: boolean;
|
||||
}
|
||||
|
||||
type QueryLog = Record<string, QueryData[]>;
|
||||
|
||||
const logStorage: QueryLog = {};
|
||||
|
||||
export const logQuery = (
|
||||
invokingResource: string,
|
||||
query: string,
|
||||
executionTime: number,
|
||||
parameters?: CFXParameters
|
||||
) => {
|
||||
if (
|
||||
executionTime >= mysql_slow_query_warning ||
|
||||
(mysql_debug && (!Array.isArray(mysql_debug) || mysql_debug.includes(invokingResource)))
|
||||
) {
|
||||
console.log(
|
||||
`${dbVersion} ^3${invokingResource} took ${executionTime.toFixed(4)}ms to execute a query!\n${query}${
|
||||
parameters ? ` ${JSON.stringify(parameters)}` : ''
|
||||
}^0`
|
||||
);
|
||||
}
|
||||
|
||||
if (!mysql_ui) return;
|
||||
|
||||
if (!logStorage[invokingResource]) logStorage[invokingResource] = [];
|
||||
else if (logStorage[invokingResource].length > mysql_log_size) logStorage[invokingResource].splice(0, 1);
|
||||
|
||||
logStorage[invokingResource].push({
|
||||
query,
|
||||
executionTime,
|
||||
date: Date.now(),
|
||||
slow: executionTime >= mysql_slow_query_warning ? true : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
RegisterCommand(
|
||||
'mysql',
|
||||
(source: number) => {
|
||||
if (!mysql_ui) return;
|
||||
|
||||
if (source < 1) {
|
||||
// source is 0 when received from the server
|
||||
console.log('^3This command cannot run server side^0');
|
||||
return;
|
||||
}
|
||||
|
||||
let totalQueries: number = 0;
|
||||
let totalTime = 0;
|
||||
let slowQueries = 0;
|
||||
let chartData: { labels: string[]; data: { queries: number; time: number }[] } = { labels: [], data: [] };
|
||||
|
||||
for (const resource in logStorage) {
|
||||
const queries = logStorage[resource];
|
||||
let totalResourceTime = 0;
|
||||
|
||||
totalQueries += queries.length;
|
||||
totalTime += queries.reduce((totalTime, query) => (totalTime += query.executionTime), 0);
|
||||
slowQueries += queries.reduce((slowQueries, query) => (slowQueries += query.slow ? 1 : 0), 0);
|
||||
totalResourceTime += queries.reduce((totalResourceTime, query) => (totalResourceTime += query.executionTime), 0);
|
||||
chartData.labels.push(resource);
|
||||
chartData.data.push({ queries: queries.length, time: totalResourceTime });
|
||||
}
|
||||
|
||||
emitNet(`oxmysql:openUi`, source, {
|
||||
resources: Object.keys(logStorage),
|
||||
totalQueries,
|
||||
slowQueries,
|
||||
totalTime,
|
||||
chartData,
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const sortQueries = (queries: QueryData[], sort: { id: 'query' | 'executionTime'; desc: boolean }) => {
|
||||
const sortedQueries = [...queries].sort((a, b) => {
|
||||
switch (sort.id) {
|
||||
case 'query':
|
||||
return a.query > b.query ? 1 : -1;
|
||||
case 'executionTime':
|
||||
return a.executionTime - b.executionTime;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return sort.desc ? sortedQueries.reverse() : sortedQueries;
|
||||
};
|
||||
|
||||
onNet(
|
||||
`oxmysql:fetchResource`,
|
||||
(data: {
|
||||
resource: string;
|
||||
pageIndex: number;
|
||||
search: string;
|
||||
sortBy?: { id: 'query' | 'executionTime'; desc: boolean }[];
|
||||
}) => {
|
||||
if (typeof data.resource !== 'string' || !IsPlayerAceAllowed(source as unknown as string, 'command.mysql')) return;
|
||||
|
||||
if (data.search) data.search = data.search.toLowerCase();
|
||||
|
||||
const resourceLog = data.search
|
||||
? logStorage[data.resource].filter((q) => q.query.toLowerCase().includes(data.search))
|
||||
: logStorage[data.resource];
|
||||
|
||||
const sort = data.sortBy && data.sortBy.length > 0 ? data.sortBy[0] : false;
|
||||
const startRow = data.pageIndex * 10;
|
||||
const endRow = startRow + 10;
|
||||
const queries = sort ? sortQueries(resourceLog, sort).slice(startRow, endRow) : resourceLog.slice(startRow, endRow);
|
||||
const pageCount = Math.ceil(resourceLog.length / 10);
|
||||
|
||||
if (!queries) return;
|
||||
|
||||
let resourceTime = 0;
|
||||
let resourceSlowQueries = 0;
|
||||
const resourceQueriesCount = resourceLog.length;
|
||||
|
||||
for (let i = 0; i < resourceQueriesCount; i++) {
|
||||
const query = resourceLog[i];
|
||||
|
||||
resourceTime += query.executionTime;
|
||||
if (query.slow) resourceSlowQueries += 1;
|
||||
}
|
||||
|
||||
emitNet(`oxmysql:loadResource`, source, {
|
||||
queries,
|
||||
pageCount,
|
||||
resourceQueriesCount,
|
||||
resourceSlowQueries,
|
||||
resourceTime,
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mysql_debug } from 'config';
|
||||
import type { MySql } from 'database';
|
||||
import { logQuery } from 'logger';
|
||||
import type { RowDataPacket } from 'mysql2';
|
||||
import type { CFXParameters } from 'types';
|
||||
|
||||
const profilerStatements = [
|
||||
'SET profiling_history_size = 0',
|
||||
'SET profiling = 0',
|
||||
'SET profiling_history_size = 100',
|
||||
'SET profiling = 1',
|
||||
];
|
||||
|
||||
/**
|
||||
* Executes MySQL queries to enable accurate query profiling results when `mysql_debug` is enabled.
|
||||
*/
|
||||
export async function runProfiler(connection: MySql, invokingResource: string) {
|
||||
if (!mysql_debug) return;
|
||||
|
||||
if (Array.isArray(mysql_debug) && !mysql_debug.includes(invokingResource)) return;
|
||||
|
||||
for (const statement of profilerStatements) await connection.query(statement);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the duration of the last 100 queries and resets profiling history.
|
||||
*/
|
||||
export async function profileBatchStatements(
|
||||
connection: MySql,
|
||||
invokingResource: string,
|
||||
query: string | { query: string; params?: CFXParameters }[],
|
||||
parameters: CFXParameters | null,
|
||||
offset: number
|
||||
) {
|
||||
const profiler = <RowDataPacket[]>(
|
||||
await connection.query(
|
||||
'SELECT FORMAT(SUM(DURATION) * 1000, 4) AS `duration` FROM INFORMATION_SCHEMA.PROFILING GROUP BY QUERY_ID'
|
||||
)
|
||||
);
|
||||
|
||||
for (const statement of profilerStatements) await connection.query(statement);
|
||||
|
||||
if (profiler.length === 0) return;
|
||||
|
||||
if (typeof query === 'string' && parameters) {
|
||||
for (let i = 0; i < profiler.length; i++) {
|
||||
logQuery(invokingResource, query, parseFloat(profiler[i].duration), parameters[offset + i]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof query === 'object') {
|
||||
for (let i = 0; i < profiler.length; i++) {
|
||||
const transaction = query[offset + i];
|
||||
|
||||
if (!transaction) break;
|
||||
|
||||
logQuery(invokingResource, transaction.query, parseFloat(profiler[i].duration), transaction.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noImplicitAny": true,
|
||||
"module": "es2020",
|
||||
"target": "es2021",
|
||||
"lib": ["es2021", "esnext.disposable"],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strictFunctionTypes": true,
|
||||
"moduleResolution": "bundler",
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"strict": true,
|
||||
"types": ["@types/node", "@citizenfx/server"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { OkPacket, ResultSetHeader, RowDataPacket, ProcedureCallPacket } from 'mysql2';
|
||||
|
||||
export type QueryResponse =
|
||||
| OkPacket
|
||||
| ResultSetHeader
|
||||
| ResultSetHeader[]
|
||||
| RowDataPacket[]
|
||||
| RowDataPacket[][]
|
||||
| OkPacket[]
|
||||
| ProcedureCallPacket;
|
||||
|
||||
export type QueryType = 'execute' | 'insert' | 'update' | 'scalar' | 'single' | null;
|
||||
|
||||
export type TransactionQuery = {
|
||||
query: string | string[];
|
||||
parameters?: CFXParameters;
|
||||
values?: CFXParameters;
|
||||
};
|
||||
|
||||
// working with this type is impossible but at least we can pretend to be strictly typed
|
||||
export type CFXParameters = any[];
|
||||
|
||||
export type CFXCallback = (result: unknown, err?: string) => void;
|
||||
@@ -0,0 +1,37 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
(() => {
|
||||
if (GetConvarInt('mysql_versioncheck', 1) === 0) return;
|
||||
|
||||
const resourceName = GetCurrentResourceName();
|
||||
const currentVersion = GetResourceMetadata(resourceName, 'version', 0)?.match(/(\d+)\.(\d+)\.(\d+)/);
|
||||
|
||||
if (!currentVersion) return;
|
||||
|
||||
setTimeout(async () => {
|
||||
const response = await fetch(`https://api.github.com/repos/overextended/oxmysql/releases/latest`).catch((err) => {
|
||||
console.warn(`Failed to retrieve latest version of oxmysql (${err.code}).`);
|
||||
});
|
||||
|
||||
if (response?.status !== 200) return;
|
||||
|
||||
const release = (await response.json()) as any;
|
||||
if (release.prerelease) return;
|
||||
|
||||
const latestVersion = release.tag_name.match(/(\d+)\.(\d+)\.(\d+)/);
|
||||
if (!latestVersion || latestVersion[0] === currentVersion[0]) return;
|
||||
|
||||
for (let i = 1; i < currentVersion.length; i++) {
|
||||
const current = parseInt(currentVersion[i]);
|
||||
const latest = parseInt(latestVersion[i]);
|
||||
|
||||
if (current !== latest) {
|
||||
if (current < latest)
|
||||
return console.log(
|
||||
`^3An update is available for ${resourceName} (current version: ${currentVersion[0]})\r\n${release.html_url}^0`
|
||||
);
|
||||
else break;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
})();
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { CFXParameters } from '../types';
|
||||
import { convertNamedPlaceholders } from '../config';
|
||||
|
||||
export const parseArguments = (query: string, parameters?: CFXParameters): [string, CFXParameters] => {
|
||||
if (typeof query !== 'string') throw new Error(`Expected query to be a string but received ${typeof query} instead.`);
|
||||
|
||||
if (convertNamedPlaceholders && parameters && typeof parameters === 'object' && !Array.isArray(parameters))
|
||||
if (query.includes(':') || query.includes('@')) {
|
||||
[query, parameters] = convertNamedPlaceholders(query, parameters);
|
||||
}
|
||||
|
||||
if (!parameters || typeof parameters === 'function') parameters = [];
|
||||
|
||||
const placeholders = query.match(/\?(?!\?)/g)?.length ?? 0;
|
||||
|
||||
if (parameters && !Array.isArray(parameters)) {
|
||||
let arr: unknown[] = [];
|
||||
|
||||
for (let i = 0; i < placeholders; i++) {
|
||||
arr[i] = parameters[i + 1] ?? null;
|
||||
}
|
||||
|
||||
parameters = arr;
|
||||
} else {
|
||||
if (placeholders) {
|
||||
if (parameters.length === 0) {
|
||||
for (let i = 0; i < placeholders; i++) parameters[i] = null;
|
||||
return [query, parameters];
|
||||
}
|
||||
|
||||
const diff = placeholders - parameters.length;
|
||||
|
||||
if (diff > 0) {
|
||||
for (let i = 0; i < diff; i++) parameters[placeholders + i] = null;
|
||||
} else if (diff < 0) {
|
||||
throw new Error(`Expected ${placeholders} parameters, but received ${parameters.length}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [query, parameters];
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { CFXParameters } from '../types';
|
||||
|
||||
export const executeType = (query: string) => {
|
||||
if (typeof query !== 'string') throw new Error(`Expected query to be a string but received ${typeof query} instead.`);
|
||||
|
||||
switch (query.substring(0, query.indexOf(' '))) {
|
||||
case 'INSERT':
|
||||
return 'insert';
|
||||
case 'UPDATE':
|
||||
return 'update';
|
||||
case 'DELETE':
|
||||
return 'update';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseExecute = (placeholders: number, parameters: CFXParameters) => {
|
||||
const parametersType = typeof parameters;
|
||||
|
||||
if (!parameters || parametersType !== 'object') return [];
|
||||
|
||||
if (!Array.isArray(parameters)) {
|
||||
if (typeof parameters === 'object') {
|
||||
const arr: unknown[] = [];
|
||||
Object.entries(parameters).forEach((entry) => (arr[parseInt(entry[0]) - 1] = entry[1]));
|
||||
parameters = arr;
|
||||
} else throw new Error(`Parameters expected an array but received ${typeof parameters} instead`);
|
||||
}
|
||||
|
||||
if (!parameters.every(Array.isArray)) {
|
||||
if (parameters.every((item) => typeof item === 'object')) {
|
||||
const arr: unknown[][] = [];
|
||||
|
||||
parameters.forEach((value, index) => {
|
||||
arr[index] = new Array(placeholders);
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
Object.entries(value).forEach((entry) => {
|
||||
arr[index][parseInt(entry[0]) - 1] = entry[1];
|
||||
});
|
||||
} else arr[index] = parameters[index];
|
||||
|
||||
for (let i = 0; i < placeholders; i++) {
|
||||
if (arr[index][i] === undefined) arr[index][i] = null;
|
||||
}
|
||||
});
|
||||
|
||||
parameters = arr;
|
||||
} else parameters = [[...parameters]];
|
||||
}
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
export const parseValues = (placeholders: number, parameters: CFXParameters) => {
|
||||
if (!Array.isArray(parameters)) {
|
||||
if (typeof parameters === 'object') {
|
||||
const arr: unknown[] = [];
|
||||
Object.entries(parameters).forEach((entry) => (arr[parseInt(entry[0]) - 1] = entry[1]));
|
||||
parameters = arr;
|
||||
} else throw new Error(`Parameters expected an array but received ${typeof parameters} instead`);
|
||||
} else if (placeholders > parameters.length) {
|
||||
for (let i = parameters.length; i < placeholders; i++) {
|
||||
parameters[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||
import type { QueryResponse, QueryType } from '../types';
|
||||
|
||||
export const parseResponse = (type: QueryType, result: QueryResponse): any => {
|
||||
switch (type) {
|
||||
case 'insert':
|
||||
return (result as ResultSetHeader)?.insertId ?? null;
|
||||
|
||||
case 'update':
|
||||
return (result as ResultSetHeader)?.affectedRows ?? null;
|
||||
|
||||
case 'single':
|
||||
return (result as RowDataPacket[])?.[0] ?? null;
|
||||
|
||||
case 'scalar':
|
||||
const row = (result as RowDataPacket[])?.[0];
|
||||
return (row && Object.values(row)[0]) ?? null;
|
||||
|
||||
default:
|
||||
return result ?? null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CFXParameters, TransactionQuery } from '../types';
|
||||
import { parseArguments } from './parseArguments';
|
||||
|
||||
const isTransactionQuery = (query: TransactionQuery | string): query is TransactionQuery =>
|
||||
(query as TransactionQuery).query !== undefined;
|
||||
|
||||
export const parseTransaction = (queries: TransactionQuery, parameters: CFXParameters) => {
|
||||
if (!Array.isArray(queries)) throw new Error(`Transaction queries must be array, received '${typeof queries}'.`);
|
||||
|
||||
if (!parameters || typeof parameters === 'function') parameters = [];
|
||||
|
||||
if (Array.isArray(queries[0])) {
|
||||
const transactions = queries.map((query) => {
|
||||
if (typeof query[1] !== 'object')
|
||||
throw new Error(`Transaction parameters must be array or object, received '${typeof query[1]}'.`);
|
||||
const [parsedQuery, parsedParameters] = parseArguments(query[0], query[1]);
|
||||
|
||||
return { query: parsedQuery, params: parsedParameters };
|
||||
});
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
const transactions = queries.map((query) => {
|
||||
const [parsedQuery, parsedParameters] = parseArguments(
|
||||
isTransactionQuery(query) ? query.query : query,
|
||||
isTransactionQuery(query) ? query.parameters || query.values : parameters
|
||||
);
|
||||
|
||||
return { query: parsedQuery, params: parsedParameters };
|
||||
});
|
||||
|
||||
return transactions;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const resourceName = GetCurrentResourceName();
|
||||
|
||||
export async function scheduleTick() {
|
||||
ScheduleResourceTick(resourceName);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { CFXCallback, CFXParameters } from '../types';
|
||||
|
||||
export const setCallback = (parameters?: CFXParameters | CFXCallback, cb?: CFXCallback) => {
|
||||
if (cb && typeof cb === 'function') return cb;
|
||||
if (parameters && typeof parameters === 'function') return parameters;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { TypeCastField, TypeCastNext } from 'mysql2/promise';
|
||||
|
||||
const BINARY_CHARSET = 63;
|
||||
|
||||
/**
|
||||
* node-mysql2 v3.9.0 introduced (breaking) typecasting for execute methods.
|
||||
*/
|
||||
export function typeCastExecute(field: TypeCastField, next: TypeCastNext) {
|
||||
switch (field.type) {
|
||||
case 'DATETIME':
|
||||
case 'DATETIME2':
|
||||
case 'TIMESTAMP':
|
||||
case 'TIMESTAMP2':
|
||||
case 'NEWDATE': {
|
||||
const value = field.string();
|
||||
return value ? new Date(value).getTime() : null;
|
||||
}
|
||||
case 'DATE': {
|
||||
const value = field.string();
|
||||
return value ? new Date(value + ' 00:00:00').getTime() : null;
|
||||
}
|
||||
default:
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mysql-async compatible typecasting.
|
||||
*/
|
||||
export function typeCast(field: TypeCastField, next: TypeCastNext) {
|
||||
switch (field.type) {
|
||||
case 'DATETIME':
|
||||
case 'DATETIME2':
|
||||
case 'TIMESTAMP':
|
||||
case 'TIMESTAMP2':
|
||||
case 'NEWDATE': {
|
||||
const value = field.string();
|
||||
return value ? new Date(value).getTime() : null;
|
||||
}
|
||||
case 'DATE': {
|
||||
const value = field.string();
|
||||
return value ? new Date(value + ' 00:00:00').getTime() : null;
|
||||
}
|
||||
case 'TINY':
|
||||
return field.length === 1 ? field.string() === '1' : next();
|
||||
case 'BIT':
|
||||
return field.length === 1 ? field.buffer()?.[0] === 1 : field.buffer()?.[0];
|
||||
case 'TINY_BLOB':
|
||||
case 'MEDIUM_BLOB':
|
||||
case 'LONG_BLOB':
|
||||
case 'BLOB':
|
||||
if (field.charset === BINARY_CHARSET) {
|
||||
const value = field.buffer();
|
||||
if (value === null) return [value];
|
||||
return [...value];
|
||||
}
|
||||
return field.string();
|
||||
default:
|
||||
return next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OkPacket, ProcedureCallPacket, ResultSetHeader, RowDataPacket } from 'mysql2/promise';
|
||||
|
||||
const oversizedResultSet = GetConvarInt('mysql_resultset_warning', 1000);
|
||||
|
||||
export default function (
|
||||
invokingResource: string,
|
||||
query: string,
|
||||
rows:
|
||||
| OkPacket
|
||||
| ResultSetHeader
|
||||
| ResultSetHeader[]
|
||||
| RowDataPacket[]
|
||||
| RowDataPacket[][]
|
||||
| OkPacket[]
|
||||
| ProcedureCallPacket
|
||||
) {
|
||||
const length = Array.isArray(rows) ? rows.length : 0;
|
||||
|
||||
if (length < oversizedResultSet) return;
|
||||
|
||||
console.warn(`${invokingResource} executed a query with an oversized result set (${length} results)!\n${query}`);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
RegisterNetEvent('oxmysql:openUi', function(data)
|
||||
SendNUIMessage({
|
||||
action = 'openUI',
|
||||
data = data
|
||||
})
|
||||
SetNuiFocus(true, true)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('exit', function(_, cb)
|
||||
cb(true)
|
||||
SetNuiFocus(false, false)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('fetchResource', function(data, cb)
|
||||
TriggerServerEvent('oxmysql:fetchResource', data)
|
||||
cb(true)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('oxmysql:loadResource', function(data)
|
||||
SendNUIMessage({
|
||||
action = 'loadResource',
|
||||
data = data
|
||||
})
|
||||
end)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": false
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Svelte + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "oxmysql",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start:game": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.5.3",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte-check": "^2.10.3",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-svelte": "^2.47.0",
|
||||
"@tanstack/svelte-table": "8.8.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"chart.js": "^4.4.4",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-floating-ui": "^1.5.9",
|
||||
"svelte-portal": "2.2.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tinro": "^0.6.12"
|
||||
}
|
||||
}
|
||||
+1865
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Route, router } from 'tinro';
|
||||
import Resource from './pages/resource/Resource.svelte';
|
||||
import Root from './pages/root/Root.svelte';
|
||||
import { useNuiEvent } from './utils/useNuiEvent';
|
||||
import { resources, generalData, chartData } from './store';
|
||||
import { debugData } from './utils/debugData';
|
||||
import { visible } from './store';
|
||||
import { scale } from 'svelte/transition';
|
||||
import { fetchNui } from './utils/fetchNui';
|
||||
|
||||
interface OpenData {
|
||||
resources: string[];
|
||||
totalQueries: number;
|
||||
slowQueries: number;
|
||||
totalTime: number;
|
||||
chartData: {
|
||||
labels: string[];
|
||||
data: { queries: number; time: number }[];
|
||||
};
|
||||
}
|
||||
|
||||
router.mode.hash();
|
||||
router.goto('/');
|
||||
|
||||
useNuiEvent('openUI', (data: OpenData) => {
|
||||
$visible = true;
|
||||
$resources = data.resources;
|
||||
$generalData = {
|
||||
queries: data.totalQueries,
|
||||
slowQueries: data.slowQueries,
|
||||
timeQuerying: data.totalTime,
|
||||
};
|
||||
$chartData = {
|
||||
labels: data.chartData.labels,
|
||||
data: data.chartData.data,
|
||||
};
|
||||
});
|
||||
|
||||
debugData<OpenData>([
|
||||
{
|
||||
action: 'openUI',
|
||||
data: {
|
||||
resources: ['ox_core', 'oxmysql', 'ox_inventory', 'ox_doorlock', 'ox_lib', 'ox_vehicleshop', 'ox_target'],
|
||||
slowQueries: 13,
|
||||
totalQueries: 332,
|
||||
totalTime: 230123,
|
||||
chartData: {
|
||||
labels: ['oxmysql', 'ox_core', 'ox_inventory', 'ox_doorlock'],
|
||||
data: [
|
||||
{ queries: 25, time: 133 },
|
||||
{ queries: 5, time: 12 },
|
||||
{ queries: 3, time: 2 },
|
||||
{ queries: 72, time: 133 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const handleESC = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
|
||||
$visible = false;
|
||||
fetchNui('exit');
|
||||
};
|
||||
|
||||
$: $visible ? window.addEventListener('keydown', handleESC) : window.removeEventListener('keydown', handleESC);
|
||||
</script>
|
||||
|
||||
{#if $visible}
|
||||
<main
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
class="font-main flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<div class="bg-dark-800 flex h-[700px] w-[1200px] rounded-md text-white">
|
||||
<Route path="/">
|
||||
<Root />
|
||||
</Route>
|
||||
<Route path="/:resource">
|
||||
<Resource />
|
||||
</Route>
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
@import url('https://use.typekit.net/qgr5ebd.css');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #25262b;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #909296;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a6a7ab;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import './index.css';
|
||||
import App from './App.svelte';
|
||||
import { isEnvBrowser } from './utils/misc';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
});
|
||||
|
||||
if (isEnvBrowser()) {
|
||||
const root = document.getElementById('app');
|
||||
|
||||
// https://i.imgur.com/iPTAdYV.png - Night time img
|
||||
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
|
||||
root!.style.backgroundSize = 'cover';
|
||||
root!.style.backgroundRepeat = 'no-repeat';
|
||||
root!.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { fetchNui } from '../../utils/fetchNui';
|
||||
import Pagination from './components/Pagination.svelte';
|
||||
import QueryTable from './components/QueryTable.svelte';
|
||||
import ResourceHeader from './components/ResourceHeader.svelte';
|
||||
import { meta } from 'tinro';
|
||||
import { useNuiEvent } from '../../utils/useNuiEvent';
|
||||
import { queries, resourceData, type QueryData } from '../../store';
|
||||
import { debugData } from '../../utils/debugData';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { filterData } from '../../store';
|
||||
import QuerySearch from './components/QuerySearch.svelte';
|
||||
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
|
||||
|
||||
let maxPage = 0;
|
||||
|
||||
onDestroy(() => {
|
||||
$queries = [];
|
||||
$filterData.page = 0;
|
||||
});
|
||||
|
||||
interface ResourceData {
|
||||
queries: QueryData[];
|
||||
pageCount: number;
|
||||
resourceQueriesCount: number;
|
||||
resourceSlowQueries: number;
|
||||
resourceTime: number;
|
||||
}
|
||||
|
||||
debugData<ResourceData>([
|
||||
{
|
||||
action: 'loadResource',
|
||||
data: {
|
||||
queries: [
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 3, slow: false, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 23, slow: true, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 15, slow: false, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 122, slow: true, date: Date.now() },
|
||||
],
|
||||
resourceQueriesCount: 3,
|
||||
resourceSlowQueries: 2,
|
||||
resourceTime: 1342,
|
||||
pageCount: 3,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// I miss callbacks :(
|
||||
useNuiEvent('loadResource', (data: ResourceData) => {
|
||||
maxPage = data.pageCount;
|
||||
$queries = data.queries;
|
||||
$resourceData = {
|
||||
resourceQueriesCount: data.resourceQueriesCount,
|
||||
resourceSlowQueries: data.resourceSlowQueries,
|
||||
resourceTime: data.resourceTime,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col justify-between">
|
||||
<div>
|
||||
<ResourceHeader />
|
||||
<QuerySearch icon={IconSearch} />
|
||||
<QueryTable />
|
||||
</div>
|
||||
<Pagination {maxPage} />
|
||||
</div>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
|
||||
import IconChevronRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronRight.svelte';
|
||||
import IconChevronsLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsLeft.svelte';
|
||||
import IconChevronsRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsRight.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { filterData } from '../../../store';
|
||||
|
||||
export let maxPage: number;
|
||||
|
||||
onDestroy(() => (maxPage = 0));
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center gap-6 pb-5">
|
||||
<button
|
||||
disabled={$filterData.page === 0}
|
||||
on:click={() => ($filterData.page = 0)}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronsLeft />
|
||||
</button>
|
||||
<button
|
||||
disabled={$filterData.page === 0}
|
||||
on:click={() => $filterData.page--}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<p>Page {$filterData.page + 1} of {maxPage}</p>
|
||||
<button
|
||||
disabled={$filterData.page >= maxPage - 1}
|
||||
on:click={() => $filterData.page++}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</button>
|
||||
<button
|
||||
disabled={$filterData.page === maxPage - 1}
|
||||
on:click={() => ($filterData.page = maxPage - 1)}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronsRight />
|
||||
</button>
|
||||
</div>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { filterData } from '../../../store/resource';
|
||||
|
||||
let value = '';
|
||||
|
||||
$: {
|
||||
$filterData.search = value;
|
||||
$filterData.page = 0;
|
||||
}
|
||||
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-dark-600 m-4 mt-0 flex items-center rounded-md border-[1px] border-transparent p-2 outline-none transition-all duration-100 focus-within:border-cyan-600"
|
||||
>
|
||||
<div class="pr-2">
|
||||
<svelte:component this={icon} class="text-dark-300" />
|
||||
</div>
|
||||
<input type="text" bind:value class="w-full bg-transparent outline-none" placeholder="Search queries..." />
|
||||
</div>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import IconChevronDown from '@tabler/icons-svelte/dist/svelte/icons/IconChevronDown.svelte';
|
||||
import IconChevronUp from '@tabler/icons-svelte/dist/svelte/icons/IconChevronUp.svelte';
|
||||
import { queries, type QueryData } from '../../../store';
|
||||
import {
|
||||
createSvelteTable,
|
||||
getCoreRowModel,
|
||||
type TableOptions,
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
type SortingState,
|
||||
} from '@tanstack/svelte-table';
|
||||
import { writable } from 'svelte/store';
|
||||
import { meta } from 'tinro';
|
||||
import { fetchNui } from '../../../utils/fetchNui';
|
||||
import { filterData } from '../../../store';
|
||||
import QueryTooltip from './QueryTooltip.svelte';
|
||||
|
||||
const route = meta();
|
||||
|
||||
const columns: ColumnDef<QueryData, number>[] = [
|
||||
{
|
||||
accessorKey: 'query',
|
||||
header: 'Query',
|
||||
cell: (info) => info.getValue(),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'executionTime',
|
||||
header: 'Time (ms)',
|
||||
cell: (info) => info.getValue().toFixed(4),
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
|
||||
let sorting: SortingState = [];
|
||||
|
||||
const setSorting = (updater) => {
|
||||
if (updater instanceof Function) {
|
||||
sorting = updater(sorting);
|
||||
} else {
|
||||
sorting = updater;
|
||||
}
|
||||
options.update((old) => ({
|
||||
...old,
|
||||
state: {
|
||||
...old.state,
|
||||
sorting,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const options = writable<TableOptions<QueryData>>({
|
||||
data: $queries,
|
||||
columns,
|
||||
manualPagination: true,
|
||||
manualSorting: true,
|
||||
pageCount: -1,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
});
|
||||
|
||||
$: options.update((prev) => ({ ...prev, data: $queries }));
|
||||
|
||||
const table = createSvelteTable(options);
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
$: {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fetchNui('fetchResource', {
|
||||
resource: route.params.resource,
|
||||
pageIndex: $filterData.page,
|
||||
search: $filterData.search,
|
||||
sortBy: sorting,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="px-4">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
{#each $table.getHeaderGroups() as headerGroup}
|
||||
<tr>
|
||||
{#each headerGroup.headers as header}
|
||||
<th class={`bg-dark-600 select-none p-1 ${header.id === 'executionTime' ? 'w-1/4' : 'w-3/4'}`}>
|
||||
{#if !header.isPlaceholder}
|
||||
<button
|
||||
class:cursor-pointer={header.column.getCanSort()}
|
||||
class:select-none={header.column.getCanSort()}
|
||||
class="flex w-full items-center justify-center gap-1"
|
||||
on:click={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<svelte:component this={flexRender(header.column.columnDef.header, header.getContext())} />
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<IconChevronUp />
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<IconChevronDown />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $table.getRowModel().rows as row}
|
||||
<tr>
|
||||
{#each row.getVisibleCells() as cell}
|
||||
<QueryTooltip
|
||||
content={cell.getValue()}
|
||||
let:floatingRef
|
||||
let:displayTooltip
|
||||
let:hideTooltip
|
||||
disabled={cell.column.id !== 'query'}
|
||||
>
|
||||
<td
|
||||
use:floatingRef
|
||||
on:mouseenter={displayTooltip}
|
||||
on:mouseleave={hideTooltip}
|
||||
class={`${cell.column.id === 'executionTime' && 'text-center'} bg-dark-700 p-2 ${
|
||||
row.original.slow && 'text-yellow-500'
|
||||
} max-w-[200px] truncate`}
|
||||
>
|
||||
<svelte:component this={flexRender(cell.column.columnDef.cell, cell.getContext())} />
|
||||
</td>
|
||||
</QueryTooltip>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom';
|
||||
import { createFloatingActions } from 'svelte-floating-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Portal from 'svelte-portal';
|
||||
|
||||
export let content: unknown;
|
||||
export let disabled: boolean;
|
||||
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
placement: 'top',
|
||||
middleware: [offset(6), flip(), shift()],
|
||||
});
|
||||
|
||||
let display = false;
|
||||
|
||||
let timer: NodeJS.Timer;
|
||||
|
||||
const displayTooltip = () => {
|
||||
if (disabled) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
display = true;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
if (disabled) return;
|
||||
clearTimeout(timer);
|
||||
display = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<slot {floatingRef} {displayTooltip} {hideTooltip} />
|
||||
{#if display && !disabled}
|
||||
<Portal target="body">
|
||||
<div
|
||||
transition:fade={{ duration: 150 }}
|
||||
use:floatingContent
|
||||
class="absolute p-2 text-sm bg-dark-50 text-dark-400 rounded-md max-w-xl font-main"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</Portal>
|
||||
{/if}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
|
||||
|
||||
import { meta, router } from 'tinro';
|
||||
import { resourceData } from '../../../store';
|
||||
|
||||
const route = meta();
|
||||
</script>
|
||||
|
||||
<div class="p-4 grid grid-flow-col grid-cols-3 items-center ">
|
||||
<button
|
||||
on:click={() => router.goto('/')}
|
||||
class="flex p-2 w-12 bg-dark-600 text-dark-100 hover:text-white rounded-md justify-center items-center hover:bg-dark-500 outline-none border-[1px] border-transparent focus-visible:border-cyan-600"
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<p class="text-center text-lg">{route.params.resource}</p>
|
||||
<div class="text-end text-dark-100 flex flex-col text-xs">
|
||||
<p>Queries: {$resourceData.resourceQueriesCount}</p>
|
||||
<p>Time: {$resourceData.resourceTime.toFixed(4)} ms</p>
|
||||
<p class="text-yellow-500">Slow queries: {$resourceData.resourceSlowQueries}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import Search from './components/Search.svelte';
|
||||
import IconFileAnalytics from '@tabler/icons-svelte/dist/svelte/icons/IconFileAnalytics.svelte';
|
||||
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
|
||||
import IconSourceCode from '@tabler/icons-svelte/dist/svelte/icons/IconSourceCode.svelte';
|
||||
import { router } from 'tinro';
|
||||
import { search, filteredResources, generalData } from '../../store';
|
||||
import Chart from './components/Chart.svelte';
|
||||
</script>
|
||||
|
||||
<div class="p-2 w-full h-full flex justify-between gap-2">
|
||||
<div class="bg-dark-700 p-4 pr-0 flex flex-col w-2/3 rounded-md">
|
||||
<div class="pr-4 flex justify-between items-center">
|
||||
<div class="flex gap-3 items-center ">
|
||||
<p class="text-2xl">Resources</p>
|
||||
<IconSourceCode />
|
||||
</div>
|
||||
<Search icon={IconSearch} bind:value={$search} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 mt-6 overflow-y-auto pr-4">
|
||||
{#each $filteredResources as resource}
|
||||
<button
|
||||
on:click={() => router.goto(`/${resource}`)}
|
||||
class="bg-dark-600 p-3 border-[1px] outline-none border-transparent focus-visible:border-cyan-600 text-left hover:bg-dark-400 rounded-md"
|
||||
>
|
||||
{resource}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark-700 p-4 flex flex-col justify-between w-1/3 rounded-md">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-3 items-center mb-4">
|
||||
<p class="text-2xl">General data</p>
|
||||
<IconFileAnalytics />
|
||||
</div>
|
||||
<div class="flex flex-col text-dark-50">
|
||||
<p>Queries: {$generalData.queries}</p>
|
||||
<p>Time querying: {$generalData.timeQuerying.toFixed(4)} ms</p>
|
||||
<p class="text-yellow-500">Slow queries: {$generalData.slowQueries}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Chart />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Pie } from 'svelte-chartjs';
|
||||
import { Chart as ChartJS, Title, Tooltip, ArcElement, CategoryScale, Colors, type TooltipItem } from 'chart.js';
|
||||
import { chartData } from '../../../store';
|
||||
|
||||
ChartJS.register(Title, Tooltip, ArcElement, CategoryScale, Colors);
|
||||
</script>
|
||||
|
||||
<Pie
|
||||
class="self-center"
|
||||
width={256}
|
||||
height={256}
|
||||
data={{
|
||||
labels: $chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
// @ts-ignore
|
||||
data: $chartData.data,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
maintainAspectRatio: false,
|
||||
responsive: false,
|
||||
parsing: {
|
||||
key: 'time',
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
// @ts-ignore
|
||||
return `Queries: ${context.raw.queries}, Time: ${context.raw.time.toFixed(4)} ms`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="p-2 flex items-center outline-none border-[1px] border-transparent transition-all duration-100 focus-within:border-cyan-600 rounded-md bg-dark-600"
|
||||
>
|
||||
<div class="pr-2">
|
||||
<svelte:component this={icon} class="text-dark-300" />
|
||||
</div>
|
||||
<input type="text" bind:value class="bg-transparent outline-none w-full" placeholder="Search resources..." />
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const visible = writable(false);
|
||||
|
||||
export const search = writable('');
|
||||
let timeout: NodeJS.Timeout;
|
||||
export const debouncedSearch = derived(search, (value, set: (value: string) => void) => {
|
||||
timeout = setTimeout(() => set(value), 500);
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
|
||||
export const resources = writable<string[]>([]);
|
||||
|
||||
export const filteredResources = derived(
|
||||
[resources, debouncedSearch],
|
||||
([$resources, $debouncedSearch], set: (value: string[]) => void) => {
|
||||
if ($debouncedSearch === '' || !$debouncedSearch) return set($resources);
|
||||
|
||||
const query = $debouncedSearch.toLowerCase();
|
||||
|
||||
return set($resources.filter((resource) => resource.toLowerCase().includes(query)));
|
||||
}
|
||||
);
|
||||
|
||||
export const generalData = writable({
|
||||
queries: 0,
|
||||
timeQuerying: 0,
|
||||
slowQueries: 0,
|
||||
});
|
||||
|
||||
export const chartData = writable<{ labels: string[]; data: { queries: number; time: number }[] }>({
|
||||
labels: [],
|
||||
data: [],
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './general';
|
||||
export * from './resource';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface QueryData {
|
||||
date: number;
|
||||
query: string;
|
||||
executionTime: number;
|
||||
slow?: boolean;
|
||||
}
|
||||
|
||||
export const queries = writable<QueryData[]>([]);
|
||||
|
||||
export const resourceData = writable<{
|
||||
resourceQueriesCount: number;
|
||||
resourceSlowQueries: number;
|
||||
resourceTime: number;
|
||||
}>({
|
||||
resourceQueriesCount: 0,
|
||||
resourceSlowQueries: 0,
|
||||
resourceTime: 0,
|
||||
});
|
||||
|
||||
export const filterData = writable({ search: '', page: 0 });
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isEnvBrowser } from './misc';
|
||||
|
||||
interface DebugEvent<T = any> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
|
||||
* This is used when developing in browser
|
||||
*
|
||||
* @param events - The event you want to cover
|
||||
* @param timer - How long until it should trigger (ms)
|
||||
*/
|
||||
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
|
||||
if (isEnvBrowser()) {
|
||||
for (const event of events) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
action: event.action,
|
||||
data: event.data,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param eventName - The endpoint eventname to target
|
||||
* @param data - Data you wish to send in the NUI Callback
|
||||
*
|
||||
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
|
||||
*/
|
||||
|
||||
export async function fetchNui<T = any>(eventName: string, data: unknown = {}): Promise<T> {
|
||||
const options = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
const resourceName = (window as any).GetParentResourceName
|
||||
? (window as any).GetParentResourceName()
|
||||
: 'nui-frame-app';
|
||||
|
||||
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
|
||||
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
interface NuiMessage<T = unknown> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that manage events listeners for receiving data from the client scripts
|
||||
* @param action The specific `action` that should be listened for.
|
||||
* @param handler The callback function that will handle data relayed by this function
|
||||
*
|
||||
* @example
|
||||
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
|
||||
* // whatever logic you want
|
||||
* })
|
||||
*
|
||||
**/
|
||||
|
||||
export function useNuiEvent<T = unknown>(action: string, handler: (data: T) => void) {
|
||||
const eventListener = (event: MessageEvent<NuiMessage<T>>) => {
|
||||
const { action: eventAction, data } = event.data;
|
||||
|
||||
eventAction === action && handler(data);
|
||||
};
|
||||
onMount(() => window.addEventListener('message', eventListener));
|
||||
onDestroy(() => window.removeEventListener('message', eventListener));
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,svelte}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
main: 'Roboto',
|
||||
},
|
||||
colors: {
|
||||
// Mantine dark colours
|
||||
dark: {
|
||||
50: '#C1C2C5',
|
||||
100: '#A6A7AB',
|
||||
200: '#909296',
|
||||
300: '#5C5F66',
|
||||
400: '#373A40',
|
||||
500: '#2C2E33',
|
||||
600: '#25262B',
|
||||
700: '#1A1B1E',
|
||||
800: '#141517',
|
||||
900: '#101113',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: './',
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user