114 lines
3.8 KiB
JavaScript
114 lines
3.8 KiB
JavaScript
// lib/apiState.js
|
|
// GET /api/state payload builder (ARCHITECTURE.md 8.5).
|
|
//
|
|
// StateEnvelope = {
|
|
// themes: ThemeListItem[], // metadata only (NOT full token payloads)
|
|
// activeThemeId: string|null,
|
|
// activeByRole?: { [roleId:number]: string },
|
|
// manifest: TokenManifest,
|
|
// caps: { layoutMode, canImport, engine, formatVersion, sizeCap },
|
|
// ... plus active{}, layoutMode, csrfToken convenience fields for the shell.
|
|
// }
|
|
// ThemeListItem = { id, name, builtin, active, engine, updatedAt, hasLayoutTree }
|
|
//
|
|
// buildState merges builtins (builtin:true) + library rows (builtin:false) via
|
|
// themeStore.list(), annotates `active` from the LIVE pointer (read via
|
|
// getState().plugin_cfgs, no DB hit), and includes the token manifest. caps.layoutMode
|
|
// toggles the SPA canvas panel.
|
|
|
|
const { getState } = require("@saltcorn/data/db/state");
|
|
|
|
const themeStore = require("./themeStore");
|
|
const { TOKEN_SCHEMA, MAX_CSS_BYTES } = require("./tokenSchema");
|
|
const { DEFAULT_TOKENS } = require("./themeSchema");
|
|
const { PRESETS } = require("./layoutTree");
|
|
const {
|
|
PLUGIN_NAME, API_VERSION, ENGINE, FORMAT_VERSION, MAX_IMPORT_BYTES, CFG,
|
|
} = require("./constants");
|
|
|
|
|
|
// Live merged plugin config for this tenant (no DB hit). Mirrors
|
|
// activePointer.getActivePointer / cfgReaders resolution.
|
|
function livePointer() {
|
|
const cfg = getState().plugin_cfgs[PLUGIN_NAME] || {};
|
|
return {
|
|
activeThemeId: cfg[CFG.ACTIVE] || null,
|
|
activeByRole: cfg[CFG.BY_ROLE] || {},
|
|
layoutMode: !!cfg[CFG.LAYOUT_MODE],
|
|
};
|
|
}
|
|
|
|
|
|
// The server-side TokenManifest: HOW each token maps to CSS, paired with its
|
|
// default value. Structurally parallel to the SPA manifest; derived from the
|
|
// single TOKEN_SCHEMA source of truth.
|
|
function buildManifest() {
|
|
const flatDefaults = {};
|
|
for (const [sec, obj] of Object.entries(DEFAULT_TOKENS)) {
|
|
if (!obj || typeof obj !== "object") {
|
|
continue;
|
|
}
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
// camelCase leaf -> kebab-case manifest key (matches compile.flattenTokens)
|
|
const kebab = k.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
flatDefaults[kebab] = v;
|
|
}
|
|
}
|
|
const tokens = {};
|
|
for (const [key, spec] of Object.entries(TOKEN_SCHEMA)) {
|
|
tokens[key] = {
|
|
kind: spec.kind,
|
|
cssVar: spec.cssVar || null,
|
|
selector: spec.selector || null,
|
|
prop: spec.prop || null,
|
|
derive: spec.derive || null,
|
|
default: flatDefaults[key] != null ? flatDefaults[key] : null,
|
|
};
|
|
}
|
|
return { engine: ENGINE, apiVersion: API_VERSION, tokens };
|
|
}
|
|
|
|
|
|
// stored row / builtin ThemeDTO -> compact ThemeListItem (metadata only).
|
|
function toListItem(t, activeThemeId, activeByRole) {
|
|
const isActive = t.id === activeThemeId || Object.values(activeByRole).includes(t.id);
|
|
return {
|
|
id: t.id,
|
|
name: t.name,
|
|
builtin: !!t.builtin,
|
|
active: isActive,
|
|
engine: t.engine,
|
|
updatedAt: t.updated_at || null,
|
|
hasLayoutTree: !!t.layoutTree,
|
|
};
|
|
}
|
|
|
|
|
|
async function buildState(req) {
|
|
const { activeThemeId, activeByRole, layoutMode } = livePointer();
|
|
const all = await themeStore.list();
|
|
const themes = all.map((t) => toListItem(t, activeThemeId, activeByRole));
|
|
const csrfToken = (req && typeof req.csrfToken === "function") ? req.csrfToken() : "";
|
|
|
|
return {
|
|
themes,
|
|
activeThemeId,
|
|
activeByRole,
|
|
active: { activeThemeId, activeByRole },
|
|
layoutMode,
|
|
csrfToken,
|
|
manifest: buildManifest(),
|
|
layoutPresets: PRESETS.map((p) => ({ id: p.id, label: p.label, tree: structuredClone(p.tree) })),
|
|
caps: {
|
|
layoutMode,
|
|
canImport: true,
|
|
engine: ENGINE,
|
|
formatVersion: FORMAT_VERSION,
|
|
sizeCap: MAX_IMPORT_BYTES,
|
|
maxCssBytes: MAX_CSS_BYTES,
|
|
},
|
|
};
|
|
}
|
|
|
|
|
|
module.exports = { buildState, buildManifest };
|