diff --git a/package-lock.json b/package-lock.json index d309b241..77dfc775 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "genezio", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "genezio", - "version": "3.0.0", + "version": "3.0.1", "license": "GPL-3", "dependencies": { "@amplitude/analytics-node": "^1.3.5", diff --git a/package.json b/package.json index e3697547..7e0dd2e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "genezio", - "version": "3.0.0", + "version": "3.0.1", "description": "Command line utility to interact with Genezio infrastructure.", "exports": "./index.js", "type": "module", diff --git a/src/commands/analyze/command.ts b/src/commands/analyze/command.ts index 7484d8b4..66c92a04 100644 --- a/src/commands/analyze/command.ts +++ b/src/commands/analyze/command.ts @@ -22,6 +22,7 @@ import { isGenezioTypesafe, hasPostgresDependency, hasMongoDependency, + isNestjsComponent, } from "./frameworks.js"; import { generateDatabaseName, readOrAskConfig } from "../deploy/utils.js"; import { getPackageManager, PackageManagerType } from "../../packageManagers/packageManager.js"; @@ -299,6 +300,23 @@ export async function analyzeCommand(options: GenezioAnalyzeOptions) { continue; } + if (await isNestjsComponent(contents)) { + await addSSRComponentToConfig( + options.config, + { + path: componentPath, + packageManager: getPackageManager().command as PackageManagerType, + scripts: { + deploy: [`${getPackageManager().command} install`], + }, + }, + SSRFrameworkComponentType.nestjs, + ); + frameworksDetected.ssr = frameworksDetected.ssr || []; + frameworksDetected.ssr.push("nest"); + continue; + } + if (await isNuxtComponent(contents)) { await addSSRComponentToConfig( options.config, diff --git a/src/commands/analyze/frameworks.ts b/src/commands/analyze/frameworks.ts index 03b8c6cb..8e349a1a 100644 --- a/src/commands/analyze/frameworks.ts +++ b/src/commands/analyze/frameworks.ts @@ -58,7 +58,7 @@ export async function findEntryFile( const entryFile = await findFileByPatterns(componentPath, patterns, FUNCTION_EXTENSIONS); if (entryFile) { - return entryFile; + return path.relative(componentPath, entryFile); } return defaultFile; @@ -256,6 +256,17 @@ export async function isNextjsComponent(contents: Record): Promi return packageJsonContent ? "next" in (packageJsonContent.dependencies || {}) : false; } +// Checks if the project is a Nest.js component +// `contents` is a map of important file paths and their contents +export async function isNestjsComponent(contents: Record): Promise { + if (!contents["package.json"]) { + return false; + } + + const packageJsonContent = JSON.parse(contents["package.json"]) as PackageJSON; + return packageJsonContent ? "@nestjs/core" in (packageJsonContent.dependencies || {}) : false; +} + // Checks if the project is a Nuxt component // `contents` is a map of important file paths and their contents export async function isNuxtComponent(contents: Record): Promise { diff --git a/src/commands/analyze/utils.ts b/src/commands/analyze/utils.ts index d5c40d1d..dbf20ef8 100644 --- a/src/commands/analyze/utils.ts +++ b/src/commands/analyze/utils.ts @@ -135,10 +135,22 @@ export async function addServicesToConfig(configPath: string, services: YAMLServ // to be able to edit it in the least intrusive way const config = await configIOController.read(/* fillDefaults= */ false); - config.services = { - ...config.services, - ...services, - }; + config.services = config.services || {}; + + // Ensure unique types are added + const existingDatabases = config.services.databases || []; + const newDatabases = services.databases || []; + + // Add only new database types + const mergedDatabases = [ + ...existingDatabases, + ...newDatabases.filter( + (db) => !existingDatabases.some((existingDb) => existingDb.type === db.type), + ), + ]; + + // Update services with the merged databases + config.services.databases = mergedDatabases; await configIOController.write(config); } diff --git a/src/commands/deploy/command.ts b/src/commands/deploy/command.ts index a3f46601..275b48f7 100644 --- a/src/commands/deploy/command.ts +++ b/src/commands/deploy/command.ts @@ -9,6 +9,7 @@ import { nuxtNitroDeploy } from "./nuxt/deploy.js"; import { dockerDeploy } from "./docker/deploy.js"; import { PackageManagerType } from "../../packageManagers/packageManager.js"; import { YamlConfigurationIOController } from "../../projectConfiguration/yaml/v2.js"; +import { nestJsDeploy } from "./nestjs/deploy.js"; export type SSRFrameworkComponent = { path: string; @@ -47,6 +48,10 @@ export async function deployCommand(options: GenezioDeployOptions) { debugLogger.debug("Deploying Docker app"); await dockerDeploy(options); break; + case DeployType.Nest: + debugLogger.debug("Deploying Nest.js app"); + await nestJsDeploy(options); + break; } } @@ -56,6 +61,7 @@ export enum DeployType { Nitro, Nuxt, Docker, + Nest, } async function decideDeployType(options: GenezioDeployOptions): Promise { @@ -82,6 +88,9 @@ async function decideDeployType(options: GenezioDeployOptions): Promise { + throw new UserError("Failed to build the NestJS project. Check the logs above."); + }); + + const result = await deployFunction(genezioConfig, options, componentPath); + + await uploadEnvVarsFromFile( + options.env, + result.projectId, + result.projectEnvId, + componentPath, + options.stage || "prod", + genezioConfig, + SSRFrameworkComponentType.nestjs, + ); + + await uploadUserCode(genezioConfig.name, genezioConfig.region, options.stage, componentPath); + + const functionUrl = result.functions.find((f) => f.name === "function-nest")?.cloudUrl; + + if (functionUrl) { + log.info( + `The app is being deployed at ${colors.cyan(functionUrl)}. It might take a few moments to be available worldwide.`, + ); + } else { + log.warn("No deployment URL was returned."); + } +} + +async function deployFunction( + config: YamlProjectConfiguration, + options: GenezioDeployOptions, + cwd: string, +) { + const cloudProvider = await getCloudProvider(config.name); + const cloudAdapter = getCloudAdapter(cloudProvider); + const cwdRelative = path.relative(process.cwd(), cwd) || "."; + + await fs.promises + .cp( + path.join(cwdRelative, "node_modules"), + path.join(cwdRelative, "dist", "node_modules"), + { recursive: true }, + ) + .catch(() => { + throw new UserError("Failed to copy node_modules to dist directory"); + }); + + const serverFunction = { + path: ".", + name: "nest", + entry: "main.js", + type: FunctionType.httpServer, + }; + + const deployConfig: YamlProjectConfiguration = { + ...config, + backend: { + path: cwdRelative, + language: { + name: Language.js, + runtime: "nodejs20.x", + architecture: "x86_64", + packageManager: PackageManagerType.npm, + }, + functions: [serverFunction], + }, + }; + + const projectConfiguration = new ProjectConfiguration( + deployConfig, + await getCloudProvider(deployConfig.name), + { + generatorResponses: [], + classesInfo: [], + }, + ); + + const cloudInputs = await Promise.all( + projectConfiguration.functions.map((f) => functionToCloudInput(f, "dist")), + ); + + const result = await cloudAdapter.deploy( + cloudInputs, + projectConfiguration, + { stage: options.stage }, + ["nestjs"], + ); + + return result; +} diff --git a/src/commands/deploy/utils.ts b/src/commands/deploy/utils.ts index 52ecb46a..3c3378f7 100644 --- a/src/commands/deploy/utils.ts +++ b/src/commands/deploy/utils.ts @@ -279,16 +279,21 @@ export async function readOrAskProjectName(): Promise { * @returns A unique database name */ export async function generateDatabaseName(prefix: string): Promise { - const defaultDatabaseName = prefix + "-" + "db"; + const defaultDatabaseName = "my-" + prefix + "-db"; const databaseExists = await getDatabaseByName(defaultDatabaseName) - .then(() => true) + .then((response) => { + return response !== undefined; + }) .catch(() => false); if (!databaseExists) { return defaultDatabaseName; } + debugLogger.debug( + `Database ${defaultDatabaseName} already exists. Generating a new database name...`, + ); const generatedDatabaseName = prefix + "-" + @@ -806,6 +811,7 @@ export async function uploadEnvVarsFromFile( [SSRFrameworkComponentType.next]: configuration.nextjs?.environment, [SSRFrameworkComponentType.nuxt]: configuration.nitro?.environment, [SSRFrameworkComponentType.nitro]: configuration.nuxt?.environment, + [SSRFrameworkComponentType.nestjs]: configuration.nuxt?.environment, backend: configuration.backend?.environment, }[componentType] ?? configuration.backend?.environment; diff --git a/src/models/projectOptions.ts b/src/models/projectOptions.ts index b2f591ca..fc5113c1 100644 --- a/src/models/projectOptions.ts +++ b/src/models/projectOptions.ts @@ -27,6 +27,7 @@ export enum SSRFrameworkComponentType { next = "nextjs", nitro = "nitro", nuxt = "nuxt", + nestjs = "nestjs", } export enum ContainerComponentType { diff --git a/src/projectConfiguration/yaml/v2.ts b/src/projectConfiguration/yaml/v2.ts index d3929177..50075d58 100644 --- a/src/projectConfiguration/yaml/v2.ts +++ b/src/projectConfiguration/yaml/v2.ts @@ -315,6 +315,7 @@ function parseGenezioConfig(config: unknown) { backend: backendSchema.optional(), services: servicesSchema.optional(), frontend: zod.array(frontendSchema).or(frontendSchema).optional(), + nestjs: ssrFrameworkSchema.optional(), nextjs: ssrFrameworkSchema.optional(), nuxt: ssrFrameworkSchema.optional(), nitro: ssrFrameworkSchema.optional(),