import { readFile, access } from "node:fs/promises";
|
import { join } from "node:path";
|
import {
|
GLOBAL_CUSTOMER_NUMBER,
|
type ConfigurationReference,
|
type JsonValue,
|
type ResolvedConfiguration,
|
} from "@dh-software/baukasten-types";
|
|
/** Returns true if the given filesystem path exists. */
|
async function pathExists(targetPath: string): Promise<boolean> {
|
try {
|
await access(targetPath);
|
return true;
|
} catch {
|
return false;
|
}
|
}
|
|
/**
|
* Resolves a reference to a concrete file + its location, or null when no file
|
* exists. This is the SINGLE source of truth for baukasten path resolution — the
|
* permissions package reuses it to classify 14243 public/private access.
|
*
|
* Normal customers: <baseDir>/<customerNumber>/configurations/<key>.json
|
* Global 14243 folder: <baseDir>/14243/public/<key>.json (preferred)
|
* <baseDir>/14243/private/<key>.json
|
*
|
* If a global key exists in BOTH public/ and private/, the public file wins and
|
* a warning is logged to the server console.
|
*/
|
export async function tryResolveConfiguration(
|
baseDir: string,
|
reference: ConfigurationReference,
|
): Promise<ResolvedConfiguration | null> {
|
const { customerNumber, key } = reference;
|
|
if (customerNumber === GLOBAL_CUSTOMER_NUMBER) {
|
const publicPath = join(baseDir, GLOBAL_CUSTOMER_NUMBER, "public", `${key}.json`);
|
const privatePath = join(baseDir, GLOBAL_CUSTOMER_NUMBER, "private", `${key}.json`);
|
const publicExists = await pathExists(publicPath);
|
const privateExists = await pathExists(privatePath);
|
|
if (publicExists && privateExists) {
|
console.warn(
|
`[baukasten] Configuration "${key}" exists in both public/ and private/ of ` +
|
`customer ${GLOBAL_CUSTOMER_NUMBER}; using the public file.`,
|
);
|
}
|
if (publicExists) return { absolutePath: publicPath, location: "public" };
|
if (privateExists) return { absolutePath: privatePath, location: "private" };
|
return null;
|
}
|
|
const configPath = join(baseDir, customerNumber, "configurations", `${key}.json`);
|
if (await pathExists(configPath)) return { absolutePath: configPath, location: "normal" };
|
return null;
|
}
|
|
/** Resolves a reference to its absolute file path; throws when no file exists. */
|
export async function resolveConfigurationPath(
|
baseDir: string,
|
reference: ConfigurationReference,
|
): Promise<string> {
|
const resolved = await tryResolveConfiguration(baseDir, reference);
|
if (resolved === null) {
|
throw new Error(
|
`[baukasten] Configuration "${reference.key}" of customer ` +
|
`${reference.customerNumber} was not found.`,
|
);
|
}
|
return resolved.absolutePath;
|
}
|
|
/** Reads and parses a single referenced configuration JSON file. */
|
export async function readConfiguration(
|
baseDir: string,
|
reference: ConfigurationReference,
|
): Promise<JsonValue> {
|
const configPath = await resolveConfigurationPath(baseDir, reference);
|
const raw = await readFile(configPath, "utf-8");
|
try {
|
return JSON.parse(raw) as JsonValue;
|
} catch (cause) {
|
throw new Error(
|
`[baukasten] Failed to parse configuration at ${configPath}: ` +
|
`${(cause as Error).message}`,
|
);
|
}
|
}
|
|
/**
|
* Reads and parses an ordered list of references, preserving order so the result
|
* can be handed to the existing layout merge as the merge sequence.
|
*/
|
export async function readConfigurations(
|
baseDir: string,
|
references: ConfigurationReference[],
|
): Promise<JsonValue[]> {
|
return Promise.all(references.map((reference) => readConfiguration(baseDir, reference)));
|
}
|