refactor: centralize platform infra yaml parser helpers (#358)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-14 12:39:12 +08:00
committed by GitHub
parent 575fa2d7c9
commit 23daca5a24
3 changed files with 185 additions and 111 deletions
+39 -57
View File
@@ -18,9 +18,30 @@ import {
shQuote,
type FrpcSecretMaterial,
} from "./platform-infra-public-service";
import {
yamlAbsolutePathField,
yamlApiPathField,
yamlArrayOfRecords,
yamlBooleanField,
yamlEnumField,
yamlEnvKeyField,
yamlHostField,
yamlHttpsUrlField,
yamlIntegerArrayField,
yamlIntegerField,
yamlKubernetesNameField,
yamlObjectField,
yamlPgIdentifierField,
yamlPortField,
yamlRecord,
yamlSourceRefField,
yamlStringArrayField,
yamlStringField,
} from "./platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets";
const configFile = rootPath("config", "platform-infra", "langbot.yaml");
const configLabel = "config/platform-infra/langbot.yaml";
const serviceName = "langbot";
const pluginRuntimeServiceName = "langbot-plugin-runtime";
const fieldManager = "unidesk-platform-langbot";
@@ -1441,114 +1462,75 @@ function boolField(value: Record<string, unknown> | null, key: string, fallback:
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
return yamlRecord(value, path);
}
function objectField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
const prefix = path.length > 0 ? `${path}.` : "";
return asRecord(obj[key], `config/platform-infra/langbot.yaml.${prefix}${key}`);
return yamlObjectField(obj, key, configLabel, path);
}
function arrayOfRecords(value: unknown, path: string): Record<string, unknown>[] {
if (!Array.isArray(value)) throw new Error(`config/platform-infra/langbot.yaml.${path} must be an array`);
return value.map((item, index) => asRecord(item, `config/platform-infra/langbot.yaml.${path}[${index}]`));
return yamlArrayOfRecords(value, configLabel, path);
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`config/platform-infra/langbot.yaml.${prefix}${key} must be a non-empty string`);
return value.trim();
return yamlStringField(obj, key, configLabel, path);
}
function integerField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`config/platform-infra/langbot.yaml.${prefix}${key} must be an integer`);
return value;
return yamlIntegerField(obj, key, configLabel, path);
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "boolean") throw new Error(`config/platform-infra/langbot.yaml.${prefix}${key} must be a boolean`);
return value;
return yamlBooleanField(obj, key, configLabel, path);
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) throw new Error(`config/platform-infra/langbot.yaml.${prefix}${key} must be a string array`);
return value.map((item) => (item as string).trim());
return yamlStringArrayField(obj, key, configLabel, path);
}
function numberArrayField(obj: Record<string, unknown>, key: string, path: string): number[] {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isInteger(item))) throw new Error(`config/platform-infra/langbot.yaml.${prefix}${key} must be an integer array`);
return value as number[];
return yamlIntegerArrayField(obj, key, configLabel, path);
}
function enumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, path: string, values: T): T[number] {
const value = stringField(obj, key, path);
if (!(values as readonly string[]).includes(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be one of ${values.join(", ")}`);
return value as T[number];
return yamlEnumField(obj, key, configLabel, path, values);
}
function kubernetesNameField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be a Kubernetes name`);
return value;
return yamlKubernetesNameField(obj, key, configLabel, path);
}
function sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.startsWith("/") || value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be a relative source ref without ..`);
return value;
return yamlSourceRefField(obj, key, configLabel, path);
}
function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be an env key`);
return value;
return yamlEnvKeyField(obj, key, configLabel, path);
}
function pgIdentifierField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be a PostgreSQL identifier`);
return value;
return yamlPgIdentifierField(obj, key, configLabel, path);
}
function hostField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} has an unsupported host format`);
return value;
return yamlHostField(obj, key, configLabel, path);
}
function portField(obj: Record<string, unknown>, key: string, path: string): number {
const value = integerField(obj, key, path);
if (value < 1 || value > 65535) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be a TCP port`);
return value;
return yamlPortField(obj, key, configLabel, path);
}
function absolutePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!value.startsWith("/")) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be absolute`);
return value;
return yamlAbsolutePathField(obj, key, configLabel, path);
}
function apiPathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!value.startsWith("/") || value.includes("..")) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be an absolute path without ..`);
return value;
return yamlApiPathField(obj, key, configLabel, path);
}
function httpsUrlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
const url = new URL(value);
if (url.protocol !== "https:" || url.search || url.hash) throw new Error(`config/platform-infra/langbot.yaml.${path}.${key} must be an https URL without query or hash`);
return value.replace(/\/+$/u, "");
return yamlHttpsUrlField(obj, key, configLabel, path);
}
function pvcSpec(record: Record<string, unknown>, path: string): PvcSpec {
+36 -54
View File
@@ -20,6 +20,25 @@ import {
type PublicServiceExposure,
type PublicServiceTarget,
} from "./platform-infra-public-service";
import {
yamlAbsolutePathField,
yamlArrayOfRecords,
yamlBooleanField,
yamlEnumField,
yamlEnvKeyField,
yamlHostField,
yamlHttpsUrlField,
yamlIntegerArrayField,
yamlIntegerField,
yamlKubernetesNameField,
yamlObjectField,
yamlPgIdentifierField,
yamlPortField,
yamlRecord,
yamlSourceRefField,
yamlStringArrayField,
yamlStringField,
} from "./platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets";
const configFile = rootPath("config", "platform-infra", "n8n.yaml");
@@ -1047,108 +1066,71 @@ function boolField(value: Record<string, unknown> | null, key: string, fallback:
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
return yamlRecord(value, path);
}
function objectField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
const prefix = path.length > 0 ? `${path}.` : "";
return asRecord(obj[key], `${configLabel}.${prefix}${key}`);
return yamlObjectField(obj, key, configLabel, path);
}
function arrayOfRecords(value: unknown, path: string): Record<string, unknown>[] {
if (!Array.isArray(value)) throw new Error(`${configLabel}.${path} must be an array`);
return value.map((item, index) => asRecord(item, `${configLabel}.${path}[${index}]`));
return yamlArrayOfRecords(value, configLabel, path);
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${configLabel}.${prefix}${key} must be a non-empty string`);
return value.trim();
return yamlStringField(obj, key, configLabel, path);
}
function integerField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${configLabel}.${prefix}${key} must be an integer`);
return value;
return yamlIntegerField(obj, key, configLabel, path);
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "boolean") throw new Error(`${configLabel}.${prefix}${key} must be a boolean`);
return value;
return yamlBooleanField(obj, key, configLabel, path);
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) throw new Error(`${configLabel}.${prefix}${key} must be a string array`);
return value.map((item) => (item as string).trim());
return yamlStringArrayField(obj, key, configLabel, path);
}
function numberArrayField(obj: Record<string, unknown>, key: string, path: string): number[] {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isInteger(item))) throw new Error(`${configLabel}.${prefix}${key} must be an integer array`);
return value as number[];
return yamlIntegerArrayField(obj, key, configLabel, path);
}
function enumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, path: string, values: T): T[number] {
const value = stringField(obj, key, path);
if (!(values as readonly string[]).includes(value)) throw new Error(`${configLabel}.${path}.${key} must be one of ${values.join(", ")}`);
return value as T[number];
return yamlEnumField(obj, key, configLabel, path, values);
}
function kubernetesNameField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a Kubernetes name`);
return value;
return yamlKubernetesNameField(obj, key, configLabel, path);
}
function sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.startsWith("/") || value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a relative source ref without ..`);
return value;
return yamlSourceRefField(obj, key, configLabel, path);
}
function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be an env key`);
return value;
return yamlEnvKeyField(obj, key, configLabel, path);
}
function pgIdentifierField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a PostgreSQL identifier`);
return value;
return yamlPgIdentifierField(obj, key, configLabel, path);
}
function hostField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} has an unsupported host format`);
return value;
return yamlHostField(obj, key, configLabel, path);
}
function portField(obj: Record<string, unknown>, key: string, path: string): number {
const value = integerField(obj, key, path);
if (value < 1 || value > 65535) throw new Error(`${configLabel}.${path}.${key} must be a TCP port`);
return value;
return yamlPortField(obj, key, configLabel, path);
}
function absolutePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!value.startsWith("/")) throw new Error(`${configLabel}.${path}.${key} must be absolute`);
return value;
return yamlAbsolutePathField(obj, key, configLabel, path);
}
function httpsUrlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
const url = new URL(value);
if (url.protocol !== "https:" || url.search || url.hash) throw new Error(`${configLabel}.${path}.${key} must be an https URL without query or hash`);
return value.replace(/\/+$/u, "");
return yamlHttpsUrlField(obj, key, configLabel, path);
}
function pvcSpec(record: Record<string, unknown>, path: string): PvcSpec {
+110
View File
@@ -180,6 +180,116 @@ export function stringListField(obj: Record<string, unknown>, key: string, path:
return value as string[];
}
export function yamlFieldLabel(configLabel: string, path: string, key: string): string {
const prefix = path.length > 0 ? `${path}.` : "";
return `${configLabel}.${prefix}${key}`;
}
export function yamlRecord(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be a YAML object`);
return value as Record<string, unknown>;
}
export function yamlObjectField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): Record<string, unknown> {
return yamlRecord(obj[key], yamlFieldLabel(configLabel, path, key));
}
export function yamlArrayOfRecords(value: unknown, configLabel: string, path: string): Record<string, unknown>[] {
if (!Array.isArray(value)) throw new Error(`${configLabel}.${path} must be an array`);
return value.map((item, index) => yamlRecord(item, `${configLabel}.${path}[${index}]`));
}
export function yamlStringField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a non-empty string`);
return value.trim();
}
export function yamlIntegerField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be an integer`);
return value;
}
export function yamlBooleanField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a boolean`);
return value;
}
export function yamlStringArrayField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string[] {
const value = obj[key];
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a string array`);
return value.map((item) => (item as string).trim());
}
export function yamlIntegerArrayField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): number[] {
const value = obj[key];
if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isInteger(item))) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be an integer array`);
return value as number[];
}
export function yamlEnumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, configLabel: string, path: string, values: T): T[number] {
const value = yamlStringField(obj, key, configLabel, path);
if (!(values as readonly string[]).includes(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be one of ${values.join(", ")}`);
return value as T[number];
}
export function yamlKubernetesNameField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a Kubernetes name`);
return value;
}
export function yamlSourceRefField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (value.startsWith("/") || value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a relative source ref without ..`);
return value;
}
export function yamlEnvKeyField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be an env key`);
return value;
}
export function yamlPgIdentifierField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a PostgreSQL identifier`);
return value;
}
export function yamlHostField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${yamlFieldLabel(configLabel, path, key)} has an unsupported host format`);
return value;
}
export function yamlPortField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): number {
const value = yamlIntegerField(obj, key, configLabel, path);
if (value < 1 || value > 65535) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be a TCP port`);
return value;
}
export function yamlAbsolutePathField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!value.startsWith("/")) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be absolute`);
return value;
}
export function yamlApiPathField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
if (!value.startsWith("/") || value.includes("..")) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be an absolute path without ..`);
return value;
}
export function yamlHttpsUrlField(obj: Record<string, unknown>, key: string, configLabel: string, path: string): string {
const value = yamlStringField(obj, key, configLabel, path);
const url = new URL(value);
if (url.protocol !== "https:" || url.search || url.hash) throw new Error(`${yamlFieldLabel(configLabel, path, key)} must be an https URL without query or hash`);
return value.replace(/\/+$/u, "");
}
export function repoRelative(path: string): string {
const rel = relative(rootPath(), path);
return rel.startsWith("..") ? path : rel;