This commit is contained in:
rgrgogu
2026-05-23 14:03:38 +08:00
parent b39e7a2939
commit bf48c95467
5 changed files with 175 additions and 87 deletions
+74 -43
View File
@@ -5,6 +5,11 @@
* *
* Author: rgrgogu * Author: rgrgogu
* Date Created: Oct. 6, 2025 * Date Created: Oct. 6, 2025
***********************************************************************************************************************************************************************
* Change History:
* DATE AUTHOR LOG DESCRIPTION
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
* May 23, 2026 rgrgogu 002 Added group_code to createGroup / updateGroup; added generateGroupCode helper
***********************************************************************************************************************************************************************/ ***********************************************************************************************************************************************************************/
const sequelize = require('../../config/db.config'); const sequelize = require('../../config/db.config');
const { Op, Sequelize } = require('sequelize'); const { Op, Sequelize } = require('sequelize');
@@ -12,24 +17,38 @@ const { Op, Sequelize } = require('sequelize');
const mdl_Users = require('../../models/users/users.mdl'); const mdl_Users = require('../../models/users/users.mdl');
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl'); const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
const R = require('../../utils/response.util'); const R = require('../../utils/response.util');
const { paginate } = require('../../utils/paginate.util'); const { paginate } = require('../../utils/paginate.util');
const { getFieldValues } = require("../../utils/fieldValues.util"); const { getFieldValues } = require('../../utils/fieldValues.util');
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes'); const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes'); const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy']; // ─── Helper — generate a unique group code ────────────────────────────────────
/**
* Generates a unique group_code in the format: <SLUG>-<4-char hex>
* e.g. "SALES-A3F1", "ONBOARD-Q1-9C2D"
* Retries up to 5 times in the unlikely event of a collision.
*/
const generateGroupCode = async (name) => {
const slug = name.toUpperCase().trim().replace(/\s+/g, '-').replace(/[^A-Z0-9\-]/g, '').slice(0, 20);
for (let i = 0; i < 5; i++) {
const suffix = Math.random().toString(16).slice(2, 6).toUpperCase();
const code = `${slug}-${suffix}`;
const exists = await mdl_UserGroups.findOne({ where: { group_code: code }, paranoid: false });
if (!exists) return code;
}
throw new Error('Could not generate a unique group code after 5 attempts.');
};
// ─── GET ALL ────────────────────────────────────────────────────────────────── // ─── GET ALL ──────────────────────────────────────────────────────────────────
exports.getGroups = async (req, res) => { exports.getGroups = async (req, res) => {
try { try {
const result = await paginate(mdl_UserGroups, req, { const result = await paginate(mdl_UserGroups, req, {
excludeAttributes: groupExclude, excludeAttributes: groupExclude,
jsonbSchemas: groupSchemas, jsonbSchemas: groupSchemas,
computedAttributes: groupComputed, computedAttributes: groupComputed,
context: "list", context: 'list',
auditOptions: { mdl_Users, parentAlias: 'UserGroup' }, auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
}); });
@@ -41,7 +60,6 @@ exports.getGroups = async (req, res) => {
}; };
// ─── GET ONE ────────────────────────────────────────────────────────────────── // ─── GET ONE ──────────────────────────────────────────────────────────────────
exports.getGroup = async (req, res) => { exports.getGroup = async (req, res) => {
try { try {
const group = await mdl_UserGroups.findByPk(req.params.gid); const group = await mdl_UserGroups.findByPk(req.params.gid);
@@ -52,13 +70,13 @@ exports.getGroup = async (req, res) => {
jsonbSchemas: usersSchemas, jsonbSchemas: usersSchemas,
jsonbColumn: 'personal_info', jsonbColumn: 'personal_info',
auditOptions: { mdl_Users, parentAlias: 'User' }, auditOptions: { mdl_Users, parentAlias: 'User' },
context: "list", context: 'list',
findOptions: { findOptions: {
include: [{ include: [{
model: mdl_UserGroupMembers, model: mdl_UserGroupMembers,
where: { group_id: req.params.gid }, where: { group_id: req.params.gid },
attributes: [], attributes: [],
required: true, required: true,
}], }],
}, },
}); });
@@ -71,14 +89,25 @@ exports.getGroup = async (req, res) => {
}; };
// ─── CREATE ─────────────────────────────────────────────────────────────────── // ─── CREATE ───────────────────────────────────────────────────────────────────
exports.createGroup = async (req, res) => { exports.createGroup = async (req, res) => {
try { try {
const { name, description } = req.body; const { name, description, group_code } = req.body;
if (!name) return R.error(res, 'Group name is required.', 400); if (!name) return R.error(res, 'Group name is required.', 400);
// Use provided group_code (uppercased via model hook), otherwise auto-generate
const code = group_code
? group_code.toUpperCase().trim()
: await generateGroupCode(name);
// Check uniqueness explicitly so we return a clear error message
const duplicate = await mdl_UserGroups.findOne({ where: { group_code: code }, paranoid: false });
if (duplicate) return R.error(res, `Group code "${code}" is already in use.`, 409);
const group = await mdl_UserGroups.create({ const group = await mdl_UserGroups.create({
name, description, createdBy: req.user.user_id, name,
description,
group_code: code,
createdBy: req.user.user_id,
}); });
return R.success(res, 'Group created.', group, 201); return R.success(res, 'Group created.', group, 201);
@@ -89,15 +118,26 @@ exports.createGroup = async (req, res) => {
}; };
// ─── UPDATE ─────────────────────────────────────────────────────────────────── // ─── UPDATE ───────────────────────────────────────────────────────────────────
exports.updateGroup = async (req, res) => { exports.updateGroup = async (req, res) => {
try { try {
const group = await mdl_UserGroups.findByPk(req.params.gid); const group = await mdl_UserGroups.findByPk(req.params.gid);
if (!group) return R.error(res, 'Group not found.', 404); if (!group) return R.error(res, 'Group not found.', 404);
const { name, description } = req.body; const { name, description, group_code } = req.body;
if (name !== undefined) group.name = name; if (name !== undefined) group.name = name;
if (description !== undefined) group.description = description; if (description !== undefined) group.description = description;
if (group_code !== undefined) {
const code = group_code.toUpperCase().trim();
const duplicate = await mdl_UserGroups.findOne({
where: { group_code: code, group_id: { [Op.ne]: group.group_id } },
paranoid: false,
});
if (duplicate) return R.error(res, `Group code "${code}" is already in use.`, 409);
group.group_code = code;
}
group.updatedBy = req.user.user_id; group.updatedBy = req.user.user_id;
await group.save(); await group.save();
@@ -109,11 +149,10 @@ exports.updateGroup = async (req, res) => {
}; };
// ─── DEACTIVATE ─────────────────────────────────────────────────────────────── // ─── DEACTIVATE ───────────────────────────────────────────────────────────────
exports.deactivateGroup = async (req, res) => { exports.deactivateGroup = async (req, res) => {
try { try {
const group = await mdl_UserGroups.findByPk(req.params.gid); const group = await mdl_UserGroups.findByPk(req.params.gid);
if (!group) return R.error(res, 'Group not found.', 404); if (!group) return R.error(res, 'Group not found.', 404);
if (!group.is_active) return R.error(res, 'Group is already deactivated.', 400); if (!group.is_active) return R.error(res, 'Group is already deactivated.', 400);
await group.update({ is_active: false, updatedBy: req.user.user_id, deletedBy: req.user.user_id }); await group.update({ is_active: false, updatedBy: req.user.user_id, deletedBy: req.user.user_id });
@@ -127,13 +166,10 @@ exports.deactivateGroup = async (req, res) => {
}; };
// ─── RESTORE ────────────────────────────────────────────────────────────────── // ─── RESTORE ──────────────────────────────────────────────────────────────────
exports.restoreGroup = async (req, res) => { exports.restoreGroup = async (req, res) => {
try { try {
const group = await mdl_UserGroups.findOne({ const group = await mdl_UserGroups.findOne({ where: { group_id: req.params.gid }, paranoid: false });
where: { group_id: req.params.gid }, paranoid: false, if (!group) return R.error(res, 'Group not found.', 404);
});
if (!group) return R.error(res, 'Group not found.', 404);
if (group.is_active) return R.error(res, 'Group is already active.', 400); if (group.is_active) return R.error(res, 'Group is already active.', 400);
await group.restore(); await group.restore();
@@ -147,14 +183,13 @@ exports.restoreGroup = async (req, res) => {
}; };
// ─── BULK DEACTIVATE ────────────────────────────────────────────────────────── // ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
exports.bulkDeactivateGroups = async (req, res) => { exports.bulkDeactivateGroups = async (req, res) => {
try { try {
const { ids } = req.body; const { ids } = req.body;
if (!Array.isArray(ids) || !ids.length) if (!Array.isArray(ids) || !ids.length)
return R.error(res, 'No group IDs provided.', 400); return R.error(res, 'No group IDs provided.', 400);
const groups = await mdl_UserGroups.findAll({ where: { group_id: ids } }); const groups = await mdl_UserGroups.findAll({ where: { group_id: ids } });
if (!groups.length) return R.error(res, 'No groups found.', 404); if (!groups.length) return R.error(res, 'No groups found.', 404);
const activeGroups = groups.filter((g) => g.is_active && !g.deletedAt); const activeGroups = groups.filter((g) => g.is_active && !g.deletedAt);
@@ -180,15 +215,14 @@ exports.bulkDeactivateGroups = async (req, res) => {
}; };
// ─── BULK RESTORE ───────────────────────────────────────────────────────────── // ─── BULK RESTORE ─────────────────────────────────────────────────────────────
exports.bulkRestoreGroups = async (req, res) => { exports.bulkRestoreGroups = async (req, res) => {
try { try {
const { ids } = req.body; const { ids } = req.body;
if (!Array.isArray(ids) || !ids.length) if (!Array.isArray(ids) || !ids.length)
return R.error(res, 'No group IDs provided.', 400); return R.error(res, 'No group IDs provided.', 400);
const groups = await mdl_UserGroups.findAll({ where: { group_id: ids }, paranoid: false }); const groups = await mdl_UserGroups.findAll({ where: { group_id: ids }, paranoid: false });
if (!groups.length) return R.error(res, 'No groups found.', 404); if (!groups.length) return R.error(res, 'No groups found.', 404);
const deletedGroups = groups.filter((g) => g.deletedAt); const deletedGroups = groups.filter((g) => g.deletedAt);
if (!deletedGroups.length) if (!deletedGroups.length)
@@ -213,14 +247,13 @@ exports.bulkRestoreGroups = async (req, res) => {
}; };
// ─── ARCHIVED ───────────────────────────────────────────────────────────────── // ─── ARCHIVED ─────────────────────────────────────────────────────────────────
exports.getArchivedGroups = async (req, res) => { exports.getArchivedGroups = async (req, res) => {
try { try {
const result = await paginate(mdl_UserGroups, req, { const result = await paginate(mdl_UserGroups, req, {
excludeAttributes: groupExclude, excludeAttributes: groupExclude,
jsonbSchemas: groupSchemas, jsonbSchemas: groupSchemas,
computedAttributes: groupComputed, computedAttributes: groupComputed,
context: "archived", context: 'archived',
auditOptions: { mdl_Users, parentAlias: 'UserGroup' }, auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
findOptions: { findOptions: {
paranoid: false, paranoid: false,
@@ -236,14 +269,12 @@ exports.getArchivedGroups = async (req, res) => {
}; };
// ─── FIELD VALUES ───────────────────────────────────────────────────────────── // ─── FIELD VALUES ─────────────────────────────────────────────────────────────
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, 'GROUP', {
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, "GROUP", { blockedFields: ['deletedAt'],
blockedFields: ["deletedAt"],
paranoid: false, paranoid: false,
}); });
// ─── MEMBERSHIP ─────────────────────────────────────────────────────────────── // ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
exports.getUsersNotInGroup = async (req, res) => { exports.getUsersNotInGroup = async (req, res) => {
try { try {
const { gid: group_id } = req.params; const { gid: group_id } = req.params;
@@ -252,7 +283,7 @@ exports.getUsersNotInGroup = async (req, res) => {
const memberIds = members.map((m) => m.user_id); const memberIds = members.map((m) => m.user_id);
const users = await mdl_Users.findAll({ const users = await mdl_Users.findAll({
where: { user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] } }, where: { user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] } },
attributes: [ attributes: [
'user_id', 'user_id',
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'], [Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
@@ -272,9 +303,9 @@ exports.getUsersInGroup = async (req, res) => {
const group = await mdl_UserGroups.findByPk(group_id, { const group = await mdl_UserGroups.findByPk(group_id, {
include: [{ include: [{
model: mdl_Users, model: mdl_Users,
as: 'members', as: 'members',
through: { attributes: [] }, through: { attributes: [] },
attributes: [ attributes: [
'user_id', 'user_id',
[Sequelize.literal(`("members"."personal_info"->'name'->>'full_name')`), 'full_name'], [Sequelize.literal(`("members"."personal_info"->'name'->>'full_name')`), 'full_name'],
@@ -293,14 +324,14 @@ exports.getUsersInGroup = async (req, res) => {
exports.addUserToGroup = async (req, res) => { exports.addUserToGroup = async (req, res) => {
try { try {
const { gid: group_id } = req.params; const { gid: group_id } = req.params;
const { user_ids } = req.body; const { user_ids } = req.body;
if (!Array.isArray(user_ids) || !user_ids.length) if (!Array.isArray(user_ids) || !user_ids.length)
return R.error(res, 'No users provided.', 400); return R.error(res, 'No users provided.', 400);
const existingUsers = await mdl_Users.findAll({ where: { user_id: user_ids }, attributes: ['user_id'] }); const existingUsers = await mdl_Users.findAll({ where: { user_id: user_ids }, attributes: ['user_id'] });
const existingIds = existingUsers.map((u) => u.user_id); const existingIds = existingUsers.map((u) => u.user_id);
const notFound = user_ids.filter((id) => !existingIds.includes(id)); const notFound = user_ids.filter((id) => !existingIds.includes(id));
if (notFound.length) if (notFound.length)
return R.error(res, `Users not found: ${notFound.join(', ')}`, 404); return R.error(res, `Users not found: ${notFound.join(', ')}`, 404);
@@ -325,7 +356,7 @@ exports.addUserToGroup = async (req, res) => {
exports.removeUserFromGroup = async (req, res) => { exports.removeUserFromGroup = async (req, res) => {
try { try {
const { gid: group_id } = req.params; const { gid: group_id } = req.params;
const { user_ids } = req.body; const { user_ids } = req.body;
if (!Array.isArray(user_ids) || !user_ids.length) if (!Array.isArray(user_ids) || !user_ids.length)
return R.error(res, 'No users provided.', 400); return R.error(res, 'No users provided.', 400);
+25 -6
View File
@@ -21,7 +21,7 @@ const { paginate } = require('../../utils/paginate.util');
const { enrichPersonalInfo } = require('../../utils/personalInfo.util'); const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
const { getFieldValues } = require("../../utils/fieldValues.util"); const { getFieldValues } = require("../../utils/fieldValues.util");
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes'); const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: userComputed } = require('../../models/users/users.attributes');
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at']; const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy']; const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
@@ -34,8 +34,18 @@ exports.getUsers = async (req, res) => {
excludeAttributes: usersExclude, excludeAttributes: usersExclude,
jsonbSchemas: usersSchemas, jsonbSchemas: usersSchemas,
jsonbColumn: 'personal_info', jsonbColumn: 'personal_info',
computedAttributes: userComputed,
context: "list", context: "list",
auditOptions: { mdl_Users, parentAlias: 'User' }, auditOptions: { mdl_Users, parentAlias: 'User' },
findOptions: {
include: [{
model: mdl_UserGroups,
as: 'groups',
through: { attributes: [] }, // hide junction columns
attributes: ['group_id', 'name', 'group_code'],
required: false, // LEFT JOIN — users with no group still appear
}],
},
}); });
return R.success(res, 'Users retrieved.', result); return R.success(res, 'Users retrieved.', result);
@@ -312,17 +322,26 @@ exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
// ─── ARCHIVED ───────────────────────────────────────────────────────────────── // ─── ARCHIVED ─────────────────────────────────────────────────────────────────
// ─── GET ARCHIVED USERS ───────────────────────────────────────────────────────
exports.getArchivedUsers = async (req, res) => { exports.getArchivedUsers = async (req, res) => {
try { try {
const result = await paginate(mdl_Users, req, { const result = await paginate(mdl_Users, req, {
excludeAttributes: usersExclude, excludeAttributes: usersExclude,
jsonbSchemas: usersSchemas, jsonbSchemas: usersSchemas,
jsonbColumn: 'personal_info', jsonbColumn: 'personal_info',
context: "archived", computedAttributes: userComputed,
auditOptions: { mdl_Users, parentAlias: 'User' }, context: 'archived',
auditOptions: { mdl_Users, parentAlias: 'User' },
findOptions: { findOptions: {
paranoid: false, paranoid: false,
where: { deletedAt: { [Op.ne]: null }, is_active: false }, where: { deletedAt: { [Op.ne]: null }, is_active: false },
include: [{
model: mdl_UserGroups,
as: 'groups',
through: { attributes: [] },
attributes: ['group_id', 'name', 'group_code'],
required: false,
}],
}, },
}); });
+39 -17
View File
@@ -28,6 +28,7 @@ const bcrypt = require('bcryptjs');
const sequelize = require('../config/db.config') const sequelize = require('../config/db.config')
const mdl_Users = require('../models/users/users.mdl'); const mdl_Users = require('../models/users/users.mdl');
const mdl_UserSessions = require('../models/users/user_sessions.mdl'); const mdl_UserSessions = require('../models/users/user_sessions.mdl');
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../models/users/user_groups.mdl')
const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util'); const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util');
const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util'); const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util');
const sendEmail = require('../services/email.service'); const sendEmail = require('../services/email.service');
@@ -53,32 +54,53 @@ const safeUser = (user, extraExclude = []) => {
// ─── Register ────────────────────────────────────────────────────────────────── // ─── Register ──────────────────────────────────────────────────────────────────
exports.register = async (req, res) => { exports.register = async (req, res) => {
const transaction = await sequelize.transaction(); const transaction = await sequelize.transaction();
try { try {
const { email, password } = req.body; const { email, password, personal_info, group_code } = req.body;
// ── Duplicate check ───────────────────────────────────────────────────────
const existing = await mdl_Users.findOne({ where: { email } }); const existing = await mdl_Users.findOne({ where: { email } });
if (existing) return R.error(res, 'Email is already registered.', 409); if (existing) return R.error(res, 'Email is already registered.', 409);
// ── Validate group_code if provided ───────────────────────────────────────
let group = null;
if (group_code) {
group = await mdl_UserGroups.findOne({
where: { group_code: group_code.toUpperCase().trim(), is_active: true },
});
if (!group) return R.error(res, 'Invalid or inactive group code.', 400);
}
// ── Create user ───────────────────────────────────────────────────────────
const hashed = await bcrypt.hash(password, 12); const hashed = await bcrypt.hash(password, 12);
const otp = generateOTP(); const otp = generateOTP();
const user = await mdl_Users.create({ const user = await mdl_Users.create({
email, email,
password: hashed, password: hashed,
otp_code: otp, otp_code: otp,
otp_expires_at: getOTPExpiry(), otp_expires_at: getOTPExpiry(),
is_active: true, is_active: true,
is_verified: false, is_verified: false,
reg_type: 'system', reg_type: 'system',
acc_type: 'user', acc_type: 'user',
createdBy: null, personal_info: personal_info ?? null,
createdBy: null,
}, { transaction }); }, { transaction });
await sendEmail({ to: email, type: "OTP", data: { otp } }); // ── Enroll into group ─────────────────────────────────────────────────────
if (group) {
await mdl_UserGroupMembers.create({
group_id: group.group_id,
user_id: user.user_id,
createdBy: null,
}, { transaction });
}
await sendEmail({ to: email, type: 'OTP', data: { otp } });
await transaction.commit(); await transaction.commit();
return R.success(res, 'Registration successful. Please check your email for the OTP.', { return R.success(res, 'Registration successful. Please check your email for the OTP.', {
email: user.email, email: user.email,
}, 201); }, 201);
+31 -15
View File
@@ -7,6 +7,11 @@
* Author: rgrgogu * Author: rgrgogu
* Date Created: Oct. 6, 2025 * Date Created: Oct. 6, 2025
*********************************************************************************************************************************************************************** ***********************************************************************************************************************************************************************
* Change History:
* DATE AUTHOR LOG DESCRIPTION
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
* May 23, 2026 rgrgogu 002 Added group_code — unique invite code for self-registration
***********************************************************************************************************************************************************************
* HOW TO USE: * HOW TO USE:
* const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/user_groups.mdl'); * const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/user_groups.mdl');
***********************************************************************************************************************************************************************/ ***********************************************************************************************************************************************************************/
@@ -16,10 +21,17 @@ const mdl_Users = require('./users.mdl');
// ─── UserGroups ──────────────────────────────────────────────────────────────── // ─── UserGroups ────────────────────────────────────────────────────────────────
const mdl_UserGroups = sequelize.define('UserGroup', { const mdl_UserGroups = sequelize.define('UserGroup', {
group_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Group ID", order: 1, hidden: true }, group_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Group ID", order: 1, hidden: true },
name: { type: DataTypes.STRING(50), allowNull: false, label: "Group Name", order: 2 }, name: { type: DataTypes.STRING(50), allowNull: false, label: "Group Name", order: 2 },
description: { type: DataTypes.TEXT, label: "Description", order: 3, hidden: true }, /**
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 4 }, * group_code — unique, human-readable invite code distributed to users.
* Used in the self-registration URL: /register?group_code=<value>
* Automatically uppercased via a beforeValidate hook below.
* Example: "SALES-2025", "ONBOARD-Q1"
*/
group_code: { type: DataTypes.STRING(50), allowNull: true, unique: true, label: "Group Code", order: 3 },
description:{ type: DataTypes.TEXT, label: "Description", order: 4, hidden: true },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 5 },
// ── Audit trails ──────────────────────────────────────────────────────────── // ── Audit trails ────────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" }, createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
@@ -28,14 +40,20 @@ const mdl_UserGroups = sequelize.define('UserGroup', {
}, { }, {
tableName: 'user_groups', tableName: 'user_groups',
timestamps: true, timestamps: true,
paranoid: true, paranoid: true,
hooks: {
// Always store group_code in uppercase to make lookups case-insensitive
beforeValidate: (group) => {
if (group.group_code) group.group_code = group.group_code.toUpperCase().trim();
},
},
}); });
// ─── Junction: UserGroupMembers ──────────────────────────────────────────────── // ─── Junction: UserGroupMembers ────────────────────────────────────────────────
const mdl_UserGroupMembers = sequelize.define('UserGroupMember', { const mdl_UserGroupMembers = sequelize.define('UserGroupMember', {
group_id: { type: DataTypes.BIGINT, primaryKey: true }, group_id: { type: DataTypes.BIGINT, primaryKey: true },
user_id: { type: DataTypes.BIGINT, primaryKey: true }, user_id: { type: DataTypes.BIGINT, primaryKey: true },
joined_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, joined_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
// ── Audit trails ──────────────────────────────────────────────────────────── // ── Audit trails ────────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" }, createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
@@ -44,12 +62,12 @@ const mdl_UserGroupMembers = sequelize.define('UserGroupMember', {
}, { }, {
tableName: 'user_group_members', tableName: 'user_group_members',
timestamps: true, timestamps: true,
paranoid: true, paranoid: true,
}); });
// ─── Associations ────────────────────────────────────────────────────────────── // ─── Associations ──────────────────────────────────────────────────────────────
mdl_Users.belongsToMany(mdl_UserGroups, { through: mdl_UserGroupMembers, foreignKey: 'user_id', otherKey: 'group_id', as: 'groups' }); mdl_Users.belongsToMany(mdl_UserGroups, { through: mdl_UserGroupMembers, foreignKey: 'user_id', otherKey: 'group_id', as: 'groups' });
mdl_UserGroups.belongsToMany(mdl_Users, { through: mdl_UserGroupMembers, foreignKey: 'group_id', otherKey: 'user_id', as: 'members', }); mdl_UserGroups.belongsToMany(mdl_Users, { through: mdl_UserGroupMembers, foreignKey: 'group_id', otherKey: 'user_id', as: 'members' });
mdl_UserGroups.hasMany(mdl_UserGroupMembers, { foreignKey: 'group_id' }); mdl_UserGroups.hasMany(mdl_UserGroupMembers, { foreignKey: 'group_id' });
mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' }); mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
@@ -57,14 +75,12 @@ mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
mdl_Users.hasMany(mdl_UserGroupMembers, { foreignKey: 'user_id' }); mdl_Users.hasMany(mdl_UserGroupMembers, { foreignKey: 'user_id' });
mdl_UserGroupMembers.belongsTo(mdl_Users, { foreignKey: 'user_id' }); mdl_UserGroupMembers.belongsTo(mdl_Users, { foreignKey: 'user_id' });
// models/users/user_groups.mdl.js — add at the bottom
mdl_UserGroups.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' }); mdl_UserGroups.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
mdl_UserGroups.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' }); mdl_UserGroups.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
mdl_UserGroups.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' }); mdl_UserGroups.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
// Self-referencing associations for audit fields
mdl_Users.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' }); mdl_Users.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
mdl_Users.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' }); mdl_Users.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
mdl_Users.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' }); mdl_Users.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
module.exports = { mdl_UserGroups, mdl_UserGroupMembers }; module.exports = { mdl_UserGroups, mdl_UserGroupMembers };
+6 -6
View File
@@ -47,12 +47,12 @@ const userExclude = [
]; ];
const computedAttributes = [ const computedAttributes = [
// { {
// key: "memberCount", key: "groups",
// label: "Members", label: "Groups",
// type: "number", type: "array", // tells the paginator this is a pre-joined association array
// order: 99, // adjust order as needed order: 5,
// }, },
]; ];
module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes }; module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes };