mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// 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
|
||||
filterable: value.filterable ?? true,
|
||||
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,
|
||||
hidden: def.hidden ?? false,
|
||||
filterable: def.filterable ?? true,
|
||||
options: resolveOptions(def.type)
|
||||
});
|
||||
}
|
||||
|
||||
// ── Audit fields in correct sequence ────────────────────────────────────────
|
||||
for (const { field, type, order, hiddenOnList, hiddenOnArchived, filterable } 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),
|
||||
filterable: filterable ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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 };
|
||||
Reference in New Issue
Block a user