From 4b0789906177b28aa420df36172df127a533cb75 Mon Sep 17 00:00:00 2001 From: Selt <83926739+seltonmt012@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:51:54 +0200 Subject: [PATCH 1/3] fix(es_extended/server/modules/createJob): only insert what is missing The jobs row was inserted unconditionally, so adding a grade to an existing job hit a duplicate primary key and rolled the whole transaction back. The guard above it only caught the case where there was nothing to add, and it read ESX.Jobs, which does not list jobs that have no grades yet. --- .../es_extended/server/modules/createJob.lua | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/[core]/es_extended/server/modules/createJob.lua b/[core]/es_extended/server/modules/createJob.lua index a5712cf4..48f55540 100644 --- a/[core]/es_extended/server/modules/createJob.lua +++ b/[core]/es_extended/server/modules/createJob.lua @@ -5,22 +5,8 @@ local NOTIFY_TYPES = { ERROR = "^5[%s]^7-^1[ERROR]^7 %s" } -local function doesJobAndGradesExist(name, grades) - if not ESX.Jobs[name] then - return false - end - - for _, grade in ipairs(grades) do - if not ESX.DoesJobExist(name, grade.grade) then - return false - end - end - - return true -end - local function generateNewJobTable(name, label, grades, jobType) - local job = { name = name, label = label, type = jobType, grades = {} } + local job = ESX.Jobs[name] or { name = name, label = label, type = jobType, grades = {} } for _, v in pairs(grades) do job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = v.skin_male or '{}', skin_female = v.skin_female or '{}' } end @@ -66,22 +52,38 @@ function ESX.CreateJob(name, label, grades, jobType) jobType = "civ" end - local currentJobExist = doesJobAndGradesExist(name, grades) + local jobExists = MySQL.scalar.await('SELECT 1 FROM `jobs` WHERE `name` = ?', { name }) ~= nil + local existingGrades = {} - if currentJobExist then - notify("ERROR",currentResourceName, 'Job or grades already exists: `%s`', name) - return success + if jobExists then + local rows = MySQL.query.await('SELECT `grade` FROM `job_grades` WHERE `job_name` = ?', { name }) + + for i = 1, #(rows or {}) do + existingGrades[rows[i].grade] = true + end end - local queries = { - { query = 'INSERT INTO `jobs` (`name`, `label`, `type`) VALUES (?, ?, ?)', values = { name, label, jobType } } - } + local queries = {} + + if not jobExists then + queries[#queries + 1] = { + query = 'INSERT INTO `jobs` (`name`, `label`, `type`) VALUES (?, ?, ?)', + values = { name, label, jobType } + } + end for _, grade in pairs(grades) do - queries[#queries + 1] = { - query = 'INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES (?, ?, ?, ?, ?, ?, ?)', - values = { name, grade.grade, grade.name, grade.label, grade.salary, grade.skin_male and json.encode(grade.skin_male) or '{}', grade.skin_female and json.encode(grade.skin_female) or '{}' } - } + if not existingGrades[grade.grade] then + queries[#queries + 1] = { + query = 'INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES (?, ?, ?, ?, ?, ?, ?)', + values = { name, grade.grade, grade.name, grade.label, grade.salary, grade.skin_male and json.encode(grade.skin_male) or '{}', grade.skin_female and json.encode(grade.skin_female) or '{}' } + } + end + end + + if not queries[1] then + notify("ERROR",currentResourceName, 'Job or grades already exists: `%s`', name) + return success end success = exports.oxmysql:transaction_async(queries) From a5623a203974032bcadbb2d73dd9492fd5e0f8d5 Mon Sep 17 00:00:00 2001 From: Selt <83926739+seltonmt012@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:50:53 +0200 Subject: [PATCH 2/3] fix(es_extended/server/modules/createJob): cache only the grades that were inserted A grade that already exists in the database is skipped for the insert, but it was still written to ESX.Jobs. A second CreateJob call therefore replaced the stored name, label and salary in memory while the row itself kept the old values, and the two only agreed again after a restart. CreateJob now caches just the grades it inserted. Label and type of an existing job come from the database for the same reason. The grade lookup compares strings, so passing grade "1" for an existing grade 1 no longer inserts a second row. --- [core]/es_extended/server/modules/createJob.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/[core]/es_extended/server/modules/createJob.lua b/[core]/es_extended/server/modules/createJob.lua index 48f55540..f27172e5 100644 --- a/[core]/es_extended/server/modules/createJob.lua +++ b/[core]/es_extended/server/modules/createJob.lua @@ -52,14 +52,17 @@ function ESX.CreateJob(name, label, grades, jobType) jobType = "civ" end - local jobExists = MySQL.scalar.await('SELECT 1 FROM `jobs` WHERE `name` = ?', { name }) ~= nil + local existingJob = MySQL.single.await('SELECT `label`, `type` FROM `jobs` WHERE `name` = ?', { name }) + local jobExists = existingJob ~= nil local existingGrades = {} if jobExists then + label, jobType = existingJob.label, existingJob.type + local rows = MySQL.query.await('SELECT `grade` FROM `job_grades` WHERE `job_name` = ?', { name }) for i = 1, #(rows or {}) do - existingGrades[rows[i].grade] = true + existingGrades[tostring(rows[i].grade)] = true end end @@ -72,8 +75,11 @@ function ESX.CreateJob(name, label, grades, jobType) } end + local newGrades = {} + for _, grade in pairs(grades) do - if not existingGrades[grade.grade] then + if not existingGrades[tostring(grade.grade)] then + newGrades[#newGrades + 1] = grade queries[#queries + 1] = { query = 'INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES (?, ?, ?, ?, ?, ?, ?)', values = { name, grade.grade, grade.name, grade.label, grade.salary, grade.skin_male and json.encode(grade.skin_male) or '{}', grade.skin_female and json.encode(grade.skin_female) or '{}' } @@ -93,7 +99,7 @@ function ESX.CreateJob(name, label, grades, jobType) return success end - ESX.Jobs[name] = generateNewJobTable(name, label, grades, jobType) + ESX.Jobs[name] = generateNewJobTable(name, label, newGrades, jobType) notify("SUCCESS", currentResourceName, 'Job created successfully: `%s`', name) From 06ac4293538b4155348584bfc651440ddb0c9b3d Mon Sep 17 00:00:00 2001 From: Selt <83926739+seltonmt012@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:55:35 +0200 Subject: [PATCH 3/3] fix(es_extended/server/modules/createJob): store the encoded skin in ESX.Jobs CreateJob wrote the caller's skin table straight into ESX.Jobs while the row got the encoded string. setJob then ran json.decode over a table and died with "bad argument #1 to 'json.decode' (string expected, got table)". A restart appeared to repair the job, because the grade came back from the database as a string. Cache the same value that goes into the row. --- [core]/es_extended/server/modules/createJob.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/[core]/es_extended/server/modules/createJob.lua b/[core]/es_extended/server/modules/createJob.lua index f27172e5..b16ade82 100644 --- a/[core]/es_extended/server/modules/createJob.lua +++ b/[core]/es_extended/server/modules/createJob.lua @@ -8,7 +8,7 @@ local NOTIFY_TYPES = { local function generateNewJobTable(name, label, grades, jobType) local job = ESX.Jobs[name] or { name = name, label = label, type = jobType, grades = {} } for _, v in pairs(grades) do - job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = v.skin_male or '{}', skin_female = v.skin_female or '{}' } + job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = v.skin_male, skin_female = v.skin_female } end return job @@ -79,10 +79,13 @@ function ESX.CreateJob(name, label, grades, jobType) for _, grade in pairs(grades) do if not existingGrades[tostring(grade.grade)] then - newGrades[#newGrades + 1] = grade + local skinMale = grade.skin_male and json.encode(grade.skin_male) or '{}' + local skinFemale = grade.skin_female and json.encode(grade.skin_female) or '{}' + + newGrades[#newGrades + 1] = { grade = grade.grade, name = grade.name, label = grade.label, salary = grade.salary, skin_male = skinMale, skin_female = skinFemale } queries[#queries + 1] = { query = 'INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES (?, ?, ?, ?, ?, ?, ?)', - values = { name, grade.grade, grade.name, grade.label, grade.salary, grade.skin_male and json.encode(grade.skin_male) or '{}', grade.skin_female and json.encode(grade.skin_female) or '{}' } + values = { name, grade.grade, grade.name, grade.label, grade.salary, skinMale, skinFemale } } end end