From 23daca5a2487017964f1a3537bd91aa1546e6f36 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:39:12 +0800 Subject: [PATCH] refactor: centralize platform infra yaml parser helpers (#358) Co-authored-by: Codex --- scripts/src/platform-infra-langbot.ts | 96 ++++++++----------- scripts/src/platform-infra-n8n.ts | 90 +++++++----------- scripts/src/platform-infra-ops-library.ts | 110 ++++++++++++++++++++++ 3 files changed, 185 insertions(+), 111 deletions(-) diff --git a/scripts/src/platform-infra-langbot.ts b/scripts/src/platform-infra-langbot.ts index 2ad9c01a..90298851 100644 --- a/scripts/src/platform-infra-langbot.ts +++ b/scripts/src/platform-infra-langbot.ts @@ -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 | null, key: string, fallback: } function asRecord(value: unknown, path: string): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`); - return value as Record; + return yamlRecord(value, path); } function objectField(obj: Record, key: string, path: string): Record { - 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[] { - 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, 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, 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, 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, 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, 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(obj: Record, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, path: string): PvcSpec { diff --git a/scripts/src/platform-infra-n8n.ts b/scripts/src/platform-infra-n8n.ts index 5a765de4..40e9dcf1 100644 --- a/scripts/src/platform-infra-n8n.ts +++ b/scripts/src/platform-infra-n8n.ts @@ -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 | null, key: string, fallback: } function asRecord(value: unknown, path: string): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`); - return value as Record; + return yamlRecord(value, path); } function objectField(obj: Record, key: string, path: string): Record { - 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[] { - 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, 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, 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, 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, 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, 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(obj: Record, 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, 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, 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, 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, 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, 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, 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, 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, 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, path: string): PvcSpec { diff --git a/scripts/src/platform-infra-ops-library.ts b/scripts/src/platform-infra-ops-library.ts index 8b6f1793..c720e64d 100644 --- a/scripts/src/platform-infra-ops-library.ts +++ b/scripts/src/platform-infra-ops-library.ts @@ -180,6 +180,116 @@ export function stringListField(obj: Record, 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 { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be a YAML object`); + return value as Record; +} + +export function yamlObjectField(obj: Record, key: string, configLabel: string, path: string): Record { + return yamlRecord(obj[key], yamlFieldLabel(configLabel, path, key)); +} + +export function yamlArrayOfRecords(value: unknown, configLabel: string, path: string): Record[] { + 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, 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, 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, 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, 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, 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(obj: Record, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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;