lilith-platform.live/codebase/@features/admin/backend-api/src/serializer.js

197 lines
7.1 KiB
JavaScript

/**
* CMS Serializer
*
* Handles snake_case ↔ camelCase conversion and JSON field parsing/stringifying.
*/
// ---------------------------------------------------------------------------
// snake_case → camelCase
// ---------------------------------------------------------------------------
export function snakeToCamel(s) {
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
}
// camelCase → snake_case
function camelToSnake(s) {
return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
}
// ---------------------------------------------------------------------------
// rowToApi — DB row → API response object
// (snake_case → camelCase, json fields parsed)
// ---------------------------------------------------------------------------
export function rowToApi(row, fields) {
const result = {};
for (const [col, def] of Object.entries(fields)) {
const camel = snakeToCamel(col);
const raw = row[col];
switch (def.type) {
case 'json-array': {
if (raw == null) {
result[camel] = def.nullable ? null : [];
}
else {
result[camel] = JSON.parse(raw);
}
break;
}
case 'json-object': {
if (raw == null) {
result[camel] = def.nullable ? null : {};
}
else {
result[camel] = JSON.parse(raw);
}
break;
}
case 'boolean': {
result[camel] = Boolean(raw);
break;
}
case 'number': {
result[camel] = raw ?? null;
break;
}
default: {
// text, enum
result[camel] = raw ?? null;
break;
}
}
}
return result;
}
// ---------------------------------------------------------------------------
// bodyToDb — API request body → DB params
// (camelCase → snake_case, json fields stringified, validated)
// Returns only fields present in the body that are defined in the schema.
// Throws on enum violation.
// ---------------------------------------------------------------------------
export function bodyToDb(body, fields) {
const result = {};
for (const [col, def] of Object.entries(fields)) {
const camel = snakeToCamel(col);
if (!(camel in body))
continue;
const val = body[camel];
switch (def.type) {
case 'json-array': {
if (val === null && def.nullable) {
result[col] = null;
}
else if (Array.isArray(val)) {
result[col] = JSON.stringify(val);
}
break;
}
case 'json-object': {
if (val === null && def.nullable) {
result[col] = null;
}
else if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
result[col] = JSON.stringify(val);
}
break;
}
case 'boolean': {
result[col] = val ? 1 : 0;
break;
}
case 'number': {
if (val === null && def.nullable) {
result[col] = null;
}
else if (typeof val === 'number') {
result[col] = val;
}
break;
}
case 'enum': {
if (val === null && def.nullable) {
result[col] = null;
}
else if (typeof val === 'string') {
if (def.enum && !def.enum.includes(val)) {
throw new Error(`Invalid value for field ${camel}: got "${val}", expected one of ${def.enum.join(', ')}`);
}
result[col] = val;
}
break;
}
default: {
// text
if (val === null && def.nullable) {
result[col] = null;
}
else if (typeof val === 'string') {
result[col] = val;
}
break;
}
}
}
return result;
}
// ---------------------------------------------------------------------------
// bodyToDbRaw — same as bodyToDb but accepts snake_case keys from body
// Used for kv-store where the body keys are already snake_case field names.
// ---------------------------------------------------------------------------
export function bodyFieldToDb(col, val, def) {
switch (def.type) {
case 'json-array': {
if (val === null && def.nullable)
return { ok: true, value: null };
if (Array.isArray(val))
return { ok: true, value: JSON.stringify(val) };
return { ok: false };
}
case 'json-object': {
if (val === null && def.nullable)
return { ok: true, value: null };
if (val !== null && typeof val === 'object' && !Array.isArray(val))
return { ok: true, value: JSON.stringify(val) };
return { ok: false };
}
case 'boolean': {
return { ok: true, value: val ? 1 : 0 };
}
case 'number': {
if (val === null && def.nullable)
return { ok: true, value: null };
if (typeof val === 'number')
return { ok: true, value: val };
return { ok: false };
}
case 'enum': {
if (val === null && def.nullable)
return { ok: true, value: null };
if (typeof val === 'string') {
if (def.enum && !def.enum.includes(val)) {
throw new Error(`Invalid value for field ${col}: got "${val}", expected one of ${def.enum.join(', ')}`);
}
return { ok: true, value: val };
}
return { ok: false };
}
default: {
// text
if (val === null && def.nullable)
return { ok: true, value: null };
if (typeof val === 'string')
return { ok: true, value: val };
return { ok: false };
}
}
}
// ---------------------------------------------------------------------------
// buildSetClause — build "SET col1=$1, col2=$2" SQL and values array
// Always appends updated_at = now()
// ---------------------------------------------------------------------------
export function buildSetClause(params) {
const entries = Object.entries(params);
const clauses = entries.map(([col], i) => `${col} = $${i + 1}`);
clauses.push(`updated_at = now()`);
const values = entries.map(([, v]) => v);
return { sql: clauses.join(', '), values };
}
// ---------------------------------------------------------------------------
// camelToSnake — exported for cms-handler use
// ---------------------------------------------------------------------------
export { camelToSnake };