UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

327 lines (326 loc) 14 kB
import path from "path"; import yaml from "yaml"; import { getFileDetails, writeToFile } from "../../utils/file.js"; import { legacyRegions } from "../../utils/configs.js"; import { isValidCron } from "cron-validator"; import { DEFAULT_ARCHITECTURE, DEFAULT_NODE_RUNTIME, supportedArchitectures, supportedNodeRuntimes, } from "../../models/projectOptions.js"; import zod from "zod"; import { log } from "../../utils/logging.js"; import { UserError, zodFormatError } from "../../errors.js"; import { Language, TriggerType } from "./models.js"; import { PackageManagerType } from "../../packageManagers/packageManager.js"; var CloudProviderIdentifier; (function (CloudProviderIdentifier) { // This was depricated in Genezio YAML v2, so we replicated the enum here. CloudProviderIdentifier["AWS"] = "aws"; CloudProviderIdentifier["GENEZIO"] = "genezio"; CloudProviderIdentifier["CAPYBARA"] = "capybara"; CloudProviderIdentifier["CAPYBARA_LINUX"] = "capybaraLinux"; })(CloudProviderIdentifier || (CloudProviderIdentifier = {})); export function getTriggerTypeFromString(string) { if (string && !TriggerType[string]) { const triggerTypes = Object.keys(TriggerType).join(", "); throw new UserError("Specified class type for " + string + " is incorrect. Accepted values: " + triggerTypes + "."); } return TriggerType[string]; } export class YamlSdkConfiguration { constructor(language, path) { this.language = language; this.path = path; } } export class YamlMethodConfiguration { constructor(name, type, cronString) { this.name = name; this.type = type ?? TriggerType.jsonrpc; this.cronString = cronString; } } export class YamlClassConfiguration { constructor(path, type, language, methods, name, fromDecorator = false) { this.fromDecorator = false; this.path = path; this.type = type; this.methods = methods; this.language = language; this.name = name; this.fromDecorator = fromDecorator; } getMethodType(methodName) { const method = this.methods.find((method) => method.name === methodName); if (!method) { return this.type; } if (method && method.type) { return method.type; } return TriggerType.jsonrpc; } } export class YamlScriptsConfiguration { constructor(preBackendDeploy, postBackendDeploy, postFrontendDeploy, preFrontendDeploy, preStartLocal, postStartLocal, preReloadLocal) { this.preBackendDeploy = preBackendDeploy; this.postBackendDeploy = postBackendDeploy; this.postFrontendDeploy = postFrontendDeploy; this.preFrontendDeploy = preFrontendDeploy; this.preStartLocal = preStartLocal; this.postStartLocal = postStartLocal; this.preReloadLocal = preReloadLocal; } } export class YamlPluginsConfiguration { constructor(astGenerator, sdkGenerator) { this.astGenerator = astGenerator; this.sdkGenerator = sdkGenerator; } } export class YamlWorkspace { constructor(backend, frontend) { this.backend = backend; this.frontend = frontend; this.rawPathBackend = backend; this.rawPathFrontend = frontend; } } export var YamlProjectConfigurationType; (function (YamlProjectConfigurationType) { YamlProjectConfigurationType[YamlProjectConfigurationType["FRONTEND"] = 0] = "FRONTEND"; YamlProjectConfigurationType[YamlProjectConfigurationType["BACKEND"] = 1] = "BACKEND"; YamlProjectConfigurationType[YamlProjectConfigurationType["ROOT"] = 2] = "ROOT"; })(YamlProjectConfigurationType || (YamlProjectConfigurationType = {})); /** * This class represents the model for the YAML configuration file. */ export class YamlProjectConfiguration { constructor(name, region, language, sdk = undefined, cloudProvider, classes, frontend = undefined, scripts = undefined, plugins = undefined, options = undefined, workspace = undefined, packageManager = undefined) { this.name = name; this.region = region; this.language = language; this.sdk = sdk; this.cloudProvider = cloudProvider; this.classes = classes; this.frontend = frontend; this.scripts = scripts; this.plugins = plugins; this.options = options; this.workspace = workspace; this.packageManager = packageManager; } getClassConfiguration(path) { const classConfiguration = this.classes?.find((classConfiguration) => classConfiguration.path === path); if (!classConfiguration) { throw new UserError("Class configuration not found for path " + path); } return classConfiguration; } static async create(configurationFileContent) { const methodConfigurationSchema = zod .object({ name: zod.string(), type: zod.nativeEnum(TriggerType).optional(), cronString: zod.string().optional(), }) .refine(({ type, cronString }) => { if (type === TriggerType.cron && cronString === undefined) return false; return true; }, "Cron methods must have a cronString property.") .refine(({ type, cronString }) => { if (type === TriggerType.cron && cronString && !isValidCron(cronString)) { return false; } return true; }, "The cronString is not valid. Check https://crontab.guru/ for more information.") .refine(({ type, cronString }) => { const cronParts = cronString?.split(" "); if (type === TriggerType.cron && cronParts && cronParts[2] != "*" && cronParts[4] != "*") { return false; } return true; }, "The day of the month and day of the week cannot be specified at the same time."); const configurationFileSchema = zod.object({ name: zod.string().refine((value) => { const nameRegex = new RegExp("^[a-zA-Z][-a-zA-Z0-9]*$"); return nameRegex.test(value); }, "Must start with a letter and contain only letters, numbers and dashes."), region: zod .enum(legacyRegions.map((r) => r.value)) .default("us-east-1"), language: zod.nativeEnum(Language).default(Language.ts), cloudProvider: zod .nativeEnum(CloudProviderIdentifier, { errorMap: (issue, ctx) => { if (issue.code === zod.ZodIssueCode.invalid_enum_value) { return { message: "Invalid enum value. The supported values are `genezio` or `selfHostedAws`.", }; } return { message: ctx.defaultError }; }, }) .default(CloudProviderIdentifier.GENEZIO), classes: zod .array(zod .object({ path: zod.string(), type: zod.nativeEnum(TriggerType).default(TriggerType.jsonrpc), name: zod.string().optional(), methods: zod.array(methodConfigurationSchema).optional(), }) // Hack to make sure that the method type is set to the class type .transform((value) => { for (const method of value.methods || []) { method.type = method.type || value.type; } return value; })) .optional(), options: zod .object({ nodeRuntime: zod.enum(supportedNodeRuntimes).default(DEFAULT_NODE_RUNTIME), architecture: zod.enum(supportedArchitectures).default(DEFAULT_ARCHITECTURE), }) .optional(), sdk: zod .object({ language: zod.string().refine((value) => { if (!Language[value]) { log.warn(`The \`sdk.language\` ${value} value, specified in your configuration, is not supported by default. It will be treated as a custom language plugin.`); } return true; }), path: zod.string(), }) .optional(), frontend: zod .object({ path: zod.string(), subdomain: zod .string() .optional() .refine((value) => { if (!value) return true; const subdomainRegex = new RegExp("^[a-zA-Z0-9-]+$"); return subdomainRegex.test(value); }, "A valid subdomain only contains letters, numbers and dashes."), }) .optional(), workspace: zod .object({ backend: zod.string(), frontend: zod.string(), }) .optional(), packageManager: zod.nativeEnum(PackageManagerType).optional(), scripts: zod .object({ preBackendDeploy: zod.string().optional(), postBackendDeploy: zod.string().optional(), postFrontendDeploy: zod.string().optional(), preFrontendDeploy: zod.string().optional(), preStartLocal: zod.string().optional(), postStartLocal: zod.string().optional(), preReloadLocal: zod.string().optional(), }) .optional(), plugins: zod .object({ astGenerator: zod.array(zod.string()), sdkGenerator: zod.array(zod.string()), }) .optional(), }); let configurationFile; try { configurationFile = configurationFileSchema.parse(configurationFileContent); } catch (e) { if (e instanceof zod.ZodError) { throw new UserError(`There was a problem parsing your YAML configuration!\n${zodFormatError(e)}`); } throw new UserError(`There was a problem parsing your YAML configuration!\n${e}`); } const unparsedClasses = configurationFile.classes || []; const classes = unparsedClasses.map((classConfiguration) => { const methods = classConfiguration.methods || []; return new YamlClassConfiguration(classConfiguration.path, classConfiguration.type, path.parse(classConfiguration.path).ext, methods.map((method) => new YamlMethodConfiguration(method.name, method.type, method.cronString)), classConfiguration.name); }); const workspace = configurationFile.workspace ? new YamlWorkspace(configurationFile.workspace.backend, configurationFile.workspace.frontend) : undefined; return new YamlProjectConfiguration(configurationFile.name, configurationFile.region, configurationFile.language, configurationFile.sdk, configurationFile.cloudProvider, classes, configurationFile.frontend, configurationFile.scripts, configurationFile.plugins, configurationFile.options, workspace, configurationFile.packageManager); } getMethodType(path, methodName) { const classElement = this.classes?.find((classElement) => { return classElement.path === path; }); return classElement?.getMethodType(methodName); } addClass(classPath, type, methods) { const language = path.parse(classPath).ext; this.classes?.push(new YamlClassConfiguration(classPath, type, language, methods)); } // The type parameter is used only if the yaml is a root type of genezio.yaml. // // this yaml mutation is becoming a mess and we should reconsider how // we implement it. // // Update: Fixed in yaml v2 async writeToFile(path = "./genezio.yaml") { const content = { name: this.name, region: this.region, language: this.language, cloudProvider: this.cloudProvider ? this.cloudProvider : undefined, options: this.options ? this.options : undefined, sdk: this.sdk, scripts: this.scripts, frontend: this.frontend ? { path: this.frontend?.path, subdomain: this.frontend?.subdomain, } : undefined, classes: this.classes.filter((c) => !c.fromDecorator).length ? this.classes?.map((c) => ({ path: c.path, type: c.type, name: c.name ? c.name : undefined, methods: c.methods.map((m) => ({ name: m.name, type: m.type, cronString: m.cronString, })), })) : undefined, packageManager: this.packageManager ? this.packageManager : undefined, workspace: this.workspace ? { backend: this.workspace.rawPathBackend, frontend: this.workspace.rawPathFrontend, } : undefined, }; const fileDetails = getFileDetails(path); const yamlString = yaml.stringify(content); await writeToFile(fileDetails.path, fileDetails.filename, yamlString).catch((error) => { log.error(error.toString()); }); } async addSubdomain(subdomain) { this.frontend = { path: this.frontend?.path || "./frontend/build", subdomain: subdomain, }; await this.writeToFile(); } }