Files
starr-philproperties/utils/modelToAttributes.util.js
T
2026-05-14 12:59:42 +08:00

171 lines
5.8 KiB
JavaScript

// utils/modelToAttributes.js
const { DataTypes } = require("sequelize");
/**
* Maps Sequelize DataType to a simple UI type string.
*/
function resolveType(dataType) {
if (!dataType) return "text";
const type = dataType.constructor?.key || dataType.key || "";
if (["BIGINT", "INTEGER", "FLOAT", "DOUBLE", "DECIMAL"].includes(type)) return "number";
if (["DATE", "DATEONLY"].includes(type)) return "date";
if (["BOOLEAN"].includes(type)) return "enum";
if (["ENUM"].includes(type)) return "enum";
if (["JSONB", "JSON"].includes(type)) return "jsonb";
return "text";
}
/**
* Resolves options (e.g. enum choices) from a Sequelize field definition.
*/
function resolveOptions(dataType) {
if (!dataType) return "text";
const type = dataType.constructor?.key || dataType.key || "";
if (type === "ENUM") {
return { choices: dataType.values ?? [] };
}
if (type === "BOOLEAN") {
return { choices: ["true", "false"] };
}
return {};
}
/**
* Converts a camelCase or snake_case field name to a readable label.
*/
function toLabel(field) {
return field
.replace(/_/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
/**
* Flattens JSONB field paths from a schema definition.
*
* @param {Object} jsonbSchema - e.g. { name: { given_name, full_name }, date_of_birth }
* @param {string} prefix - e.g. "personal_info"
* @returns {Array} flat attribute entries for each leaf path
*/
function flattenJsonb(jsonbSchema = {}, prefix = "", exclude = []) {
const result = [];
for (const [key, value] of Object.entries(jsonbSchema)) {
const path = prefix ? `${prefix}.${key}` : key;
if (exclude.includes(path)) continue;
// Nested object (no `type` key = it's a group, not a leaf)
if (value && typeof value === "object" && !value.type) {
result.push(...flattenJsonb(value, path, exclude));
} else {
result.push({
name: value?.label || toLabel(key), // <-- prefer label
type: value?.type ?? "text",
field: path,
order: value?.order ?? Infinity, // ← carry order from schema
hidden: value?.hidden ?? false, // ← carry hidden flag
options: {},
});
}
}
return result;
}
/**
* Generates an attributes array from a Sequelize model + optional JSONB schema map.
*
* @param {Object} model - Sequelize model (e.g. mdl_Users)
* @param {Object} jsonbSchemas - map of JSONB field names to their schema definition
* e.g. { personal_info: { name: { full_name: "text", given_name: "text" }, date_of_birth: "date" } }
* @param {string[]} exclude - field names to exclude (e.g. ["password", "otp_code"])
* @returns {Array} attributes array
*/
function modelToAttributes(model, { jsonbSchemas = {}, exclude = [], timestampLabels = {}, context = "" } = {}) {
const rawAttrs = model.rawAttributes || model.tableAttributes;
const attributes = [];
const defaultTimestampLabels = {
createdAt: "Created",
modifiedAt: "Modified",
updatedAt: "Modified",
deletedAt: "Deleted",
createdBy: "Created By",
updatedBy: "Modified By",
deletedBy: "Deleted By",
...timestampLabels,
};
// Audit fields excluded from normal loop — handled separately at the end
const auditFields = [
'createdAt', 'updatedAt', 'modifiedAt',
'deletedAt',
'createdBy', 'updatedBy', 'deletedBy',
];
// Ordered audit sequence
const auditSequence = [
{ field: "updatedAt", type: "date", hiddenOnList: false, hiddenOnArchived: true },
{ field: "modifiedAt", type: "date", hiddenOnList: false, hiddenOnArchived: true },
{ field: "updatedBy", type: "text", hiddenOnList: false, hiddenOnArchived: true },
{ field: "createdAt", type: "date", hiddenOnList: false, hiddenOnArchived: true },
{ field: "createdBy", type: "text", hiddenOnList: false, hiddenOnArchived: true },
{ field: "deletedAt", type: "date", hiddenOnList: true, hiddenOnArchived: false },
{ field: "deletedBy", type: "text", hiddenOnList: true, hiddenOnArchived: false },
];
// ── Normal fields (excluding audit) ─────────────────────────────────────────
for (const [field, def] of Object.entries(rawAttrs)) {
if (exclude.includes(field)) continue;
if (auditFields.includes(field)) continue; // skip audit — added later in order
const dataType = def.type;
const type = resolveType(def.type); // ← pass only the type
if (type === "jsonb" && jsonbSchemas[field]) {
attributes.push(...flattenJsonb(jsonbSchemas[field], field, exclude));
continue;
}
if (type === "jsonb") continue;
attributes.push({
name: defaultTimestampLabels[field] ?? def.label ?? toLabel(field),
type,
field,
order: def.order ?? Infinity, // ← carry order from model
hidden: def.hidden ?? false, // ← carry hidden flag
options: resolveOptions(def.type)
});
}
// ── Audit fields in correct sequence ────────────────────────────────────────
for (const { field, type, order, hiddenOnList, hiddenOnArchived } of auditSequence) {
if (exclude.includes(field)) continue;
if (!rawAttrs[field]) continue; // skip if field doesn't exist on model
const isArchived = context === "archived";
attributes.push({
name: defaultTimestampLabels[field],
type,
field,
order,
hidden: isArchived ? hiddenOnArchived : hiddenOnList,
options: resolveOptions(rawAttrs[field]?.type),
});
}
// ── Sort by order — DO NOT strip order here, paginate.util does it ───────────
return attributes.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
}
module.exports = { modelToAttributes };