keep trying

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-12 12:53:53 +08:00
parent ea3e82e54c
commit d0a64a8043
8 changed files with 55 additions and 80 deletions
-38
View File
@@ -1,38 +0,0 @@
require('dotenv').config();
const { Sequelize } = require('sequelize');
const mdl_Users = require('./models/users/users.mdl');
const sequelize = require('./config/db.config');
const qg = sequelize.getQueryInterface().queryGenerator;
const order = [[Sequelize.json('personal_info.name.full_name'), 'ASC']];
try {
const sql = qg.selectQuery(mdl_Users.getTableName(), {
model: mdl_Users,
attributes: ['user_id'],
order,
limit: 10,
offset: 0,
}, mdl_Users);
console.log('SORT SQL:\n', sql);
} catch (e) {
console.error('SORT ERROR:', e.message);
}
// filter test
const { Op } = require('sequelize');
const whereCond = Sequelize.where(Sequelize.json('personal_info.name.full_name'), { [Op.iLike]: '%a%' });
try {
const sql2 = qg.selectQuery(mdl_Users.getTableName(), {
model: mdl_Users,
attributes: ['user_id'],
where: whereCond,
limit: 10,
offset: 0,
}, mdl_Users);
console.log('FILTER SQL:\n', sql2);
} catch (e) {
console.error('FILTER ERROR:', e.message);
}
process.exit(0);
-37
View File
@@ -1,37 +0,0 @@
const { Sequelize } = require('sequelize');
const mdl_Users = require('./models/users/users.mdl');
const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/users/user_groups.mdl');
const sequelize = require('./config/db.config');
(async () => {
try {
await sequelize.authenticate();
console.log('DB connected OK');
// Find any group_id to test with
const anyGroup = await mdl_UserGroups.findOne({ attributes: ['group_id'], paranoid: false });
console.log('sample group_id:', anyGroup?.group_id);
const group_id = anyGroup?.group_id;
const order = [[Sequelize.json('personal_info.name.full_name'), 'ASC']];
const result = await mdl_Users.findAndCountAll({
attributes: ['user_id'],
order,
limit: 3,
offset: 0,
include: [{
model: mdl_UserGroupMembers,
where: { group_id },
attributes: [],
required: true,
}],
logging: (sql) => console.log('\n[SQL]', sql),
});
console.log('SORT+JOIN RESULT COUNT:', result.count, 'rows:', result.rows.length);
} catch (e) {
console.error('ERROR:', e.message);
} finally {
await sequelize.close();
}
})();
+4 -1
View File
@@ -337,7 +337,10 @@ exports.getUsersNotInGroup = async (req, res) => {
const memberIds = members.map((m) => m.user_id);
const users = await mdl_Users.findAll({
where: { user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] } },
where: {
user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] },
acc_type: 'user', // exclude staff/admin — only regular users can be added to a group
},
attributes: [
'user_id',
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
+15 -1
View File
@@ -186,6 +186,12 @@ exports.deactivateUser = async (req, res) => {
await user.update({ is_active: false, deletedBy: req.user.user_id });
await user.destroy();
await mdl_UserGroupMembers.update(
{ deletedBy: req.user.user_id },
{ where: { user_id: req.params.id } }
);
await mdl_UserGroupMembers.destroy({ where: { user_id: req.params.id } });
await mdl_UserSessions.update(
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
{ where: { user_id: req.params.id } }
@@ -226,6 +232,13 @@ exports.bulkDeactivateUsers = async (req, res) => {
{ where: { user_id: activeIds } }
);
await mdl_Users.destroy({ where: { user_id: activeIds } });
await mdl_UserGroupMembers.update(
{ deletedBy: req.user.user_id },
{ where: { user_id: activeIds } }
);
await mdl_UserGroupMembers.destroy({ where: { user_id: activeIds } });
await mdl_UserSessions.update(
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
{ where: { user_id: activeIds } }
@@ -310,6 +323,7 @@ exports.bulkRestoreUsers = async (req, res) => {
// so a user who has ever banned someone can't be nulled out or hard-deleted —
// getBannerBlockedIds() below identifies those and callers must exclude them.
async function purgeUserDependents(userIds, t) {
await mdl_UserGroupMembers.destroy({ where: { user_id: userIds }, force: true, transaction: t });
await mdl_Achievements.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_QuizAttempt.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_UserTiers.destroy({ where: { user_id: userIds }, transaction: t });
@@ -458,7 +472,7 @@ exports.terminateSession = async (req, res) => {
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
extraDateFields: ["modifiedAt"],
extraDateFields: ["modifiedAt", "ban_expires_at"],
allowJsonb: true,
selfJoin: true,
});
+2
View File
@@ -27,8 +27,10 @@ const computedAttributes = [
literal: `(
SELECT CAST(COUNT(*) AS INTEGER)
FROM "user_group_members"
JOIN "users" ON "users"."user_id" = "user_group_members"."user_id"
WHERE "user_group_members"."group_id" = "UserGroup"."group_id"
AND "user_group_members"."deletedAt" IS NULL
AND "users"."deletedAt" IS NULL
)`,
},
];
+12 -3
View File
@@ -6,7 +6,14 @@
// deleteFile(key)
//
// Required .env vars:
// S3_ENDPOINT – http://127.0.0.1:3900 (internal — always used for uploads/deletes)
// S3_ENDPOINT – address this backend uses for uploads/deletes.
// http://127.0.0.1:3900 when co-located with Garage.
// Otherwise (backend runs on a different machine than
// Garage) point this at a network-reachable address that
// terminates on Garage — e.g. the same tunneled domain as
// S3_PUBLIC_URL, since garage-anon-proxy re-signs any
// non-presigned request with real credentials before
// forwarding. Never leave this blank.
// S3_REGION – garage
// S3_ACCESS_KEY
// S3_SECRET_KEY
@@ -28,8 +35,10 @@ const credentials = {
};
// Internal client — uploads, deletes, direct streams from the server itself.
// Always targets S3_ENDPOINT: these calls originate from this machine, so the
// internal address is the correct (and only) one to use.
// Always targets S3_ENDPOINT. When this backend is co-located with Garage,
// that's a local address; when it isn't, S3_ENDPOINT must instead be a
// network-reachable address that reaches Garage (e.g. the tunneled proxy
// domain) — see the .env docs above. Do not assume co-location here.
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "garage",
+13
View File
@@ -29,6 +29,19 @@ const getFieldValues = (Model, logTag, options = {}) => async (req, res) => {
if (!allowedFields.includes(field) && !dateFields.includes(field))
return R.error(res, "Invalid or restricted field.", 400);
// ─── ENUM / BOOLEAN fields — return the canonical value set, not a DISTINCT
// scan of current rows. A DISTINCT query silently omits valid values
// that just don't happen to exist yet (e.g. is_banned when nobody is
// currently banned), which produces an incomplete filter picklist.
const rawType = Model.rawAttributes[field]?.type;
const typeKey = rawType?.constructor?.key || rawType?.key;
if (typeKey === "ENUM") {
return R.success(res, "Field values retrieved.", rawType.values ?? []);
}
if (typeKey === "BOOLEAN") {
return R.success(res, "Field values retrieved.", ["true", "false"]);
}
if (auditByFields.includes(field)) {
// Filtering must match the audit column's real (bigint) id — returning
// just the display name here previously made buildWhere() compare a
+9
View File
@@ -95,6 +95,15 @@ async function paginate(model, req, {
const jsonbExclude = excludeAttributes.filter((f) => f.includes('.'));
const jsonbAttr = jsonbColumn ? excludeJsonbPaths(jsonbColumn, jsonbExclude) : null;
// The stripped jsonbAttr projection is aliased to the same name as the raw
// column (e.g. "personal_info") — without excluding the raw column too, it
// gets selected twice under the same alias, which Postgres/CockroachDB
// accept in a plain SELECT but reject as ambiguous the moment that alias is
// referenced in ORDER BY (e.g. sorting a jsonb-path column).
if (jsonbAttr && jsonbColumn && !topLevelExclude.includes(jsonbColumn)) {
topLevelExclude.push(jsonbColumn);
}
const attributes = modelToAttributes(model, { exclude: excludeAttributes, jsonbSchemas, context });
const ALLOWED_FIELDS = attributes.map((a) => a.field);
const computedFieldKeys = computedAttributes.map((c) => c.key);