genezio
Version:
Command line utility to interact with Genezio infrastructure.
327 lines (326 loc) • 14 kB
JavaScript
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();
}
}