Category join in items
This commit is contained in:
parent
44c920d0c5
commit
82676316c6
@ -15,6 +15,7 @@
|
|||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:deploy": "prisma migrate deploy",
|
"prisma:deploy": "prisma migrate deploy",
|
||||||
"prisma:seed": "node prisma/seed.js",
|
"prisma:seed": "node prisma/seed.js",
|
||||||
|
"db:patch": "node scripts/run-sql-patch.js",
|
||||||
"pdf:preview": "node scripts/preview-pdf-templates.js",
|
"pdf:preview": "node scripts/preview-pdf-templates.js",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,6 +4,30 @@ const bcrypt = require('bcrypt');
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const ensureModulePermissions = async (roleId, { code, name, sortOrder, actions }) => {
|
||||||
|
const mod = await prisma.modules.upsert({
|
||||||
|
where: { code },
|
||||||
|
update: { name, sort_order: sortOrder, is_active: true },
|
||||||
|
create: { code, name, sort_order: sortOrder, is_active: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
const permission = await prisma.permissions.upsert({
|
||||||
|
where: { module_id_action: { module_id: mod.id, action } },
|
||||||
|
update: { is_active: true },
|
||||||
|
create: { module_id: mod.id, action, is_active: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.role_permissions.upsert({
|
||||||
|
where: {
|
||||||
|
role_id_permission_id: { role_id: roleId, permission_id: permission.id },
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
create: { role_id: roleId, permission_id: permission.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const superAdminRole = await prisma.roles.findFirst({
|
const superAdminRole = await prisma.roles.findFirst({
|
||||||
where: { name: 'Super Admin', deleted_at: null },
|
where: { name: 'Super Admin', deleted_at: null },
|
||||||
@ -13,6 +37,13 @@ async function main() {
|
|||||||
throw new Error('Super Admin role not found. Run erp_phase1_ddl.sql first.');
|
throw new Error('Super Admin role not found. Run erp_phase1_ddl.sql first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureModulePermissions(superAdminRole.id, {
|
||||||
|
code: 'REPORTS',
|
||||||
|
name: 'Reports',
|
||||||
|
sortOrder: 90,
|
||||||
|
actions: ['view', 'export'],
|
||||||
|
});
|
||||||
|
|
||||||
const passwordHash = await bcrypt.hash('Admin@123', 12);
|
const passwordHash = await bcrypt.hash('Admin@123', 12);
|
||||||
|
|
||||||
const user = await prisma.users.upsert({
|
const user = await prisma.users.upsert({
|
||||||
|
|||||||
62
scripts/run-sql-patch.js
Normal file
62
scripts/run-sql-patch.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
require('dotenv').config();
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
|
||||||
|
const fileArg = process.argv[2];
|
||||||
|
|
||||||
|
if (!fileArg) {
|
||||||
|
console.error('Usage: node scripts/run-sql-patch.js <path-to.sql>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.resolve(process.cwd(), fileArg);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(`SQL file not found: ${filePath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const splitStatements = (sql) =>
|
||||||
|
sql
|
||||||
|
.split(/;\s*(?:\r?\n|$)/)
|
||||||
|
.map((part) =>
|
||||||
|
part
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter((line) => !line.trim().startsWith('--'))
|
||||||
|
.join('\n')
|
||||||
|
.trim()
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const sql = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const statements = splitStatements(sql);
|
||||||
|
|
||||||
|
if (statements.length === 0) {
|
||||||
|
console.error('No SQL statements found in file.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Applying ${statements.length} statement(s) from ${path.basename(filePath)}...`);
|
||||||
|
|
||||||
|
for (const [index, statement] of statements.entries()) {
|
||||||
|
await prisma.$executeRawUnsafe(`${statement};`);
|
||||||
|
console.log(` [${index + 1}/${statements.length}] OK`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Patch applied successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Patch failed:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@ -114,6 +114,21 @@ components:
|
|||||||
code: { type: string, example: 'CHEM' }
|
code: { type: string, example: 'CHEM' }
|
||||||
name: { type: string, example: 'Chemicals' }
|
name: { type: string, example: 'Chemicals' }
|
||||||
is_active: { type: boolean, example: true }
|
is_active: { type: boolean, example: true }
|
||||||
|
ItemSubcategoriesResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string, example: '1' }
|
||||||
|
item_category_id: { type: string, example: '1' }
|
||||||
|
code: { type: string, example: 'CHEM' }
|
||||||
|
name: { type: string, example: 'Chemicals' }
|
||||||
|
is_active: { type: boolean, example: true }
|
||||||
|
item_category:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
id: { type: string, example: '1' }
|
||||||
|
code: { type: string, example: 'RAW' }
|
||||||
|
name: { type: string, example: 'Raw Material' }
|
||||||
ItemsCreateBody:
|
ItemsCreateBody:
|
||||||
type: object
|
type: object
|
||||||
required: [item_name, item_category_id, uom_id]
|
required: [item_name, item_category_id, uom_id]
|
||||||
@ -702,6 +717,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
tags: [Item Subcategories]
|
tags: [Item Subcategories]
|
||||||
summary: List Item Subcategories
|
summary: List Item Subcategories
|
||||||
|
description: Each row includes joined `item_category` (`id`, `code`, `name`).
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: page
|
name: page
|
||||||
@ -715,10 +731,23 @@ paths:
|
|||||||
- in: query
|
- in: query
|
||||||
name: is_active
|
name: is_active
|
||||||
schema: { type: boolean }
|
schema: { type: boolean }
|
||||||
|
- in: query
|
||||||
|
name: item_category_id
|
||||||
|
schema: { type: integer }
|
||||||
|
description: Filter by parent item category
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: List fetched
|
description: List fetched (includes item_category join)
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/ApiResponse"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/ItemSubcategoriesResponse" }
|
||||||
post:
|
post:
|
||||||
tags: [Item Subcategories]
|
tags: [Item Subcategories]
|
||||||
summary: Create Item Subcategorie
|
summary: Create Item Subcategorie
|
||||||
@ -743,8 +772,15 @@ paths:
|
|||||||
summary: Get Item Subcategorie by id
|
summary: Get Item Subcategorie by id
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Fetched
|
description: Fetched (includes item_category join)
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/ApiResponse"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data: { $ref: "#/components/schemas/ItemSubcategoriesResponse" }
|
||||||
"404": { description: Not found }
|
"404": { description: Not found }
|
||||||
put:
|
put:
|
||||||
tags: [Item Subcategories]
|
tags: [Item Subcategories]
|
||||||
|
|||||||
@ -28,8 +28,11 @@ const buildMasterService = ({
|
|||||||
fields,
|
fields,
|
||||||
uniqueField = null,
|
uniqueField = null,
|
||||||
softDelete = true,
|
softDelete = true,
|
||||||
|
include = undefined,
|
||||||
|
mapRow = (row) => row,
|
||||||
}) => {
|
}) => {
|
||||||
const model = prisma[modelName];
|
const model = prisma[modelName];
|
||||||
|
const withInclude = include ? { include } : {};
|
||||||
|
|
||||||
const createOne = async (payload, userId, requestId) => {
|
const createOne = async (payload, userId, requestId) => {
|
||||||
const data = normalizePayload(payload, fields);
|
const data = normalizePayload(payload, fields);
|
||||||
@ -49,17 +52,18 @@ const buildMasterService = ({
|
|||||||
if (fields.some((f) => f.name === 'updated_by'))
|
if (fields.some((f) => f.name === 'updated_by'))
|
||||||
data.updated_by = userId ? BigInt(userId) : null;
|
data.updated_by = userId ? BigInt(userId) : null;
|
||||||
|
|
||||||
const created = await model.create({ data });
|
const created = await model.create({ data, ...withInclude });
|
||||||
|
const mapped = mapRow(created);
|
||||||
await auditLog({
|
await auditLog({
|
||||||
tableName,
|
tableName,
|
||||||
recordId: created.id,
|
recordId: created.id,
|
||||||
action: 'CREATE',
|
action: 'CREATE',
|
||||||
oldValue: null,
|
oldValue: null,
|
||||||
newValue: created,
|
newValue: mapped,
|
||||||
userId,
|
userId,
|
||||||
requestId,
|
requestId,
|
||||||
});
|
});
|
||||||
return created;
|
return mapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = async (query) => {
|
const list = async (query) => {
|
||||||
@ -85,20 +89,21 @@ const buildMasterService = ({
|
|||||||
where[f.name] = f.type === 'int' ? BigInt(query[f.name]) : query[f.name];
|
where[f.name] = f.type === 'int' ? BigInt(query[f.name]) : query[f.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [rows, total] = await Promise.all([
|
||||||
model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }),
|
model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit, ...withInclude }),
|
||||||
model.count({ where }),
|
model.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { data, meta: { page, limit, total } };
|
return { data: rows.map(mapRow), meta: { page, limit, total } };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOne = async (id) => {
|
const getOne = async (id) => {
|
||||||
const one = await model.findFirst({
|
const one = await model.findFirst({
|
||||||
where: { id: BigInt(id), ...(softDelete ? { deleted_at: null } : {}) },
|
where: { id: BigInt(id), ...(softDelete ? { deleted_at: null } : {}) },
|
||||||
|
...withInclude,
|
||||||
});
|
});
|
||||||
if (!one) throw new ApiError(404, `${tableName} not found`);
|
if (!one) throw new ApiError(404, `${tableName} not found`);
|
||||||
return one;
|
return mapRow(one);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateOne = async (id, payload, userId, requestId) => {
|
const updateOne = async (id, payload, userId, requestId) => {
|
||||||
@ -107,19 +112,20 @@ const buildMasterService = ({
|
|||||||
if (fields.some((f) => f.name === 'updated_by'))
|
if (fields.some((f) => f.name === 'updated_by'))
|
||||||
data.updated_by = userId ? BigInt(userId) : null;
|
data.updated_by = userId ? BigInt(userId) : null;
|
||||||
|
|
||||||
const updated = await model.update({ where: { id: BigInt(id) }, data });
|
const updated = await model.update({ where: { id: BigInt(id) }, data, ...withInclude });
|
||||||
|
const mapped = mapRow(updated);
|
||||||
|
|
||||||
await auditLog({
|
await auditLog({
|
||||||
tableName,
|
tableName,
|
||||||
recordId: id,
|
recordId: id,
|
||||||
action: 'UPDATE',
|
action: 'UPDATE',
|
||||||
oldValue: existing,
|
oldValue: existing,
|
||||||
newValue: updated,
|
newValue: mapped,
|
||||||
userId,
|
userId,
|
||||||
requestId,
|
requestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return updated;
|
return mapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeOne = async (id, userId, requestId) => {
|
const removeOne = async (id, userId, requestId) => {
|
||||||
|
|||||||
@ -1,16 +1,33 @@
|
|||||||
const { buildMasterService } = require('../_shared/master.factory');
|
const { buildMasterService } = require('../_shared/master.factory');
|
||||||
|
|
||||||
|
const subcategoryInclude = {
|
||||||
|
item_categories: { select: { id: true, code: true, name: true } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeSubcategory = (row) => {
|
||||||
|
if (!row) return null;
|
||||||
|
const { item_categories, ...rest } = row;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
item_category: item_categories || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
modelName: 'item_subcategories',
|
modelName: 'item_subcategories',
|
||||||
tableName: 'item_subcategories',
|
tableName: 'item_subcategories',
|
||||||
uniqueField: 'code',
|
uniqueField: 'code',
|
||||||
softDelete: true,
|
softDelete: true,
|
||||||
|
include: subcategoryInclude,
|
||||||
|
mapRow: sanitizeSubcategory,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'item_category_id',
|
name: 'item_category_id',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
|
asBigInt: true,
|
||||||
uppercase: false,
|
uppercase: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'code',
|
name: 'code',
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
const { masterCode, masterName, listQuerySchema: baseListQuerySchema } = require('../_shared/masters.validation');
|
||||||
|
|
||||||
const createSchema = Joi.object({
|
const createSchema = Joi.object({
|
||||||
item_category_id: Joi.number().integer().optional(),
|
item_category_id: Joi.number().integer().positive().required(),
|
||||||
code: masterCode({ max: 30 }),
|
code: masterCode({ max: 30 }),
|
||||||
name: masterName({ max: 150 }),
|
name: masterName({ max: 150 }),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSchema = Joi.object({
|
const updateSchema = Joi.object({
|
||||||
item_category_id: Joi.number().integer().optional(),
|
item_category_id: Joi.number().integer().positive().optional(),
|
||||||
code: masterCode({ max: 30 }),
|
code: masterCode({ max: 30 }),
|
||||||
name: masterName({ max: 150 }),
|
name: masterName({ max: 150 }),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
}).min(1);
|
}).min(1);
|
||||||
|
|
||||||
|
const listQuerySchema = baseListQuerySchema.keys({
|
||||||
|
item_category_id: Joi.number().integer().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user