diff --git a/apps/api/v2/Dockerfile b/apps/api/v2/Dockerfile index d06763fce4..77d7c7c18f 100644 --- a/apps/api/v2/Dockerfile +++ b/apps/api/v2/Dockerfile @@ -12,12 +12,13 @@ ENV NODE_ENV="production" ENV NODE_OPTIONS="--max-old-space-size=8192" ENV DATABASE_DIRECT_URL=${DATABASE_DIRECT_URL} ENV DATABASE_URL=${DATABASE_URL} +ENV USE_POOL="true" COPY . . RUN yarn install RUN yarn workspace @calcom/api-v2 run generate-schemas -RUN yarn workspace @calcom/platform-libraries run build +RUN yarn workspace @calcom/api-v2 run build:docker RUN yarn workspace @calcom/api-v2 run build EXPOSE 80 diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index df86d48779..07c9f0f97a 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -6,7 +6,8 @@ "license": "UNLICENSED", "private": true, "scripts": { - "build": "yarn dev:build && nest build", + "build": "nest build", + "build:docker": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build && yarn workspace @calcom/trpc build:server", "format": "biome format --write src test", "start": "nest start", "dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"", @@ -47,7 +48,7 @@ "@microsoft/microsoft-graph-types-beta": "0.42.0-preview", "@nest-lab/throttler-storage-redis": "1.0.0", "@nestjs/axios": "4.0.0", - "@nestjs/bull": "10.1.1", + "@nestjs/bull": "11.0.4", "@nestjs/common": "10.4.20", "@nestjs/config": "3.2.0", "@nestjs/core": "10.4.20", diff --git a/apps/api/v2/src/bootstrap.ts b/apps/api/v2/src/bootstrap.ts index 34384cc731..3c2c2a46ff 100644 --- a/apps/api/v2/src/bootstrap.ts +++ b/apps/api/v2/src/bootstrap.ts @@ -10,7 +10,7 @@ import { X_CAL_SECRET_KEY, } from "@calcom/platform-constants"; import type { ValidationError } from "@nestjs/common"; -import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common"; +import { BadRequestException, Logger, ValidationPipe, VersioningType } from "@nestjs/common"; import type { NestExpressApplication } from "@nestjs/platform-express"; import cookieParser from "cookie-parser"; import { Request } from "express"; @@ -21,62 +21,68 @@ import { ZodExceptionFilter } from "@/filters/zod-exception.filter"; import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter"; import { TRPCExceptionFilter } from "./filters/trpc-exception.filter"; +const logger: Logger = new Logger("Bootstrap"); + export const bootstrap = (app: NestExpressApplication): NestExpressApplication => { - app.enableShutdownHooks(); - app.enableVersioning({ - type: VersioningType.CUSTOM, - extractor: (request: unknown) => { - const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined; - if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) { - return headerVersion; - } - return VERSION_2024_04_15; - }, - defaultVersion: VERSION_2024_04_15, - }); - app.use(helmet()); - app.enableCors({ - origin: "*", - methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"], - allowedHeaders: [ - X_CAL_CLIENT_ID, - X_CAL_SECRET_KEY, - X_CAL_PLATFORM_EMBED, - CAL_API_VERSION_HEADER, - "Accept", - "Authorization", - "Content-Type", - "Origin", - ], - maxAge: 86_400, - }); - - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - validationError: { - target: true, - value: true, + try { + if (!process.env.VERCEL) { + app.enableShutdownHooks(); + } + app.enableVersioning({ + type: VersioningType.CUSTOM, + extractor: (request: unknown) => { + const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined; + if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) { + return headerVersion; + } + return VERSION_2024_04_15; }, - exceptionFactory(errors: ValidationError[]): BadRequestException { - return new BadRequestException({ errors }); - }, - }) - ); + defaultVersion: VERSION_2024_04_15, + }); + app.use(helmet()); + app.enableCors({ + origin: "*", + methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"], + allowedHeaders: [ + X_CAL_CLIENT_ID, + X_CAL_SECRET_KEY, + X_CAL_PLATFORM_EMBED, + CAL_API_VERSION_HEADER, + "Accept", + "Authorization", + "Content-Type", + "Origin", + ], + maxAge: 86_400, + }); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + validationError: { + target: true, + value: true, + }, + exceptionFactory(errors: ValidationError[]): BadRequestException { + return new BadRequestException({ errors }); + }, + }) + ); + // Exception filters, new filters go at the bottom, keep the order + app.useGlobalFilters(new PrismaExceptionFilter()); + app.useGlobalFilters(new ZodExceptionFilter()); + app.useGlobalFilters(new HttpExceptionFilter()); + app.useGlobalFilters(new TRPCExceptionFilter()); + app.useGlobalFilters(new CalendarServiceExceptionFilter()); + app.use(cookieParser()); - // Exception filters, new filters go at the bottom, keep the order - app.useGlobalFilters(new PrismaExceptionFilter()); - app.useGlobalFilters(new ZodExceptionFilter()); - app.useGlobalFilters(new HttpExceptionFilter()); - app.useGlobalFilters(new TRPCExceptionFilter()); - app.useGlobalFilters(new CalendarServiceExceptionFilter()); + if (process?.env?.API_GLOBAL_PREFIX) { + app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX); + } - app.use(cookieParser()); - - if (process?.env?.API_GLOBAL_PREFIX) { - app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX); + return app; + } catch (error) { + logger.error("Error starting NestJS app:", error); + throw error; } - - return app; }; diff --git a/apps/api/v2/src/main.ts b/apps/api/v2/src/main.ts index 3540c8ca94..10599bf4b1 100644 --- a/apps/api/v2/src/main.ts +++ b/apps/api/v2/src/main.ts @@ -1,26 +1,64 @@ import "dotenv/config"; import { IncomingMessage, Server, ServerResponse } from "node:http"; + import { Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { NestFactory } from "@nestjs/core"; import type { NestExpressApplication } from "@nestjs/platform-express"; +import type { Express, Request, Response } from "express"; import { WinstonModule } from "nest-winston"; import qs from "qs"; import type { AppConfig } from "@/config/type"; + import { AppModule } from "./app.module"; import { bootstrap } from "./bootstrap"; import { loggerConfig } from "./lib/logger"; -run().catch((error: Error) => { - console.error("Failed to start Cal Platform API", { error: error.stack }); - process.exit(1); -}); +const logger: Logger = new Logger("App"); + +/** + * Singleton Class to manage the NestJS App instance. + * Ensures we only initialize the app once per container lifecycle. + */ +class NestServer { + private static server: Express; // The underlying Express instance + + private constructor() {} + + /** + * Returns the cached server instance. + * If it doesn't exist, it creates, bootstraps, and initializes it. + */ + public static async getInstance(): Promise { + if (!NestServer.server) { + const app = await createNestApp(); + + // Execute bootstrap (Pipes, Interceptors, CORS, etc.) + bootstrap(app); + + // Initialize the app (connects to DB, resolves modules) + await app.init(); + + // extract the Express instance to pass to Vercel + NestServer.server = app.getHttpAdapter().getInstance(); + } + return NestServer.server; + } +} + +// ----------------------------------------------------------------------------- +// LOCAL DEVELOPMENT STARTUP +// ----------------------------------------------------------------------------- +if (!process.env.VERCEL) { + run().catch((error: Error) => { + logger.error("Failed to start Cal Platform API", { error: error.stack }); + process.exit(1); + }); +} async function run(): Promise { const app = await createNestApp(); - const logger = new Logger("App"); - try { bootstrap(app); const config = app.get(ConfigService); @@ -32,23 +70,50 @@ async function run(): Promise { } await app.listen(port); - logger.log(`Application started on port: ${port}`); + logger.log(`Application started locally on port: ${port}`); } catch (error) { - console.error(error); - logger.error("Application crashed", { - error, - }); + logger.error("Application crashed during local startup", { error }); } } +// ----------------------------------------------------------------------------- +// VERCEL SERVERLESS HANDLER +// ----------------------------------------------------------------------------- +export default async (req: Request, res: Response): Promise => { + try { + const server = await NestServer.getInstance(); + + // Vercel/AWS specific: Re-parse query strings to support array formats + // (e.g., ?ids[]=1&ids[]=2) which Vercel's native parser might simplify. + if (req.url) { + const [_path, queryString] = req.url.split("?"); + if (queryString) { + req.query = qs.parse(queryString, { arrayLimit: 1000 }); + } + } + + // Delegate request to the cached Express instance + return server(req, res); + } catch (error) { + logger.error("Critical: Failed to initialize NestJS Serverless instance", error); + res.statusCode = 500; + res.end("Internal Server Error: Initialization Failed"); + } +}; + +// ----------------------------------------------------------------------------- +// APP FACTORY +// ----------------------------------------------------------------------------- export async function createNestApp(): Promise< NestExpressApplication> > { const app = await NestFactory.create(AppModule, { logger: WinstonModule.createLogger(loggerConfig()), + // Preserved as requested: bodyParser: false, }); + // Custom query parser configuration for the underlying Express app app.set("query parser", (str: string) => qs.parse(str, { arrayLimit: 1000 })); return app; diff --git a/apps/api/v2/src/modules/prisma/prisma-read.service.ts b/apps/api/v2/src/modules/prisma/prisma-read.service.ts index ca28e9c049..1d0b935918 100644 --- a/apps/api/v2/src/modules/prisma/prisma-read.service.ts +++ b/apps/api/v2/src/modules/prisma/prisma-read.service.ts @@ -54,8 +54,9 @@ export class PrismaReadService implements OnModuleInit, OnModuleDestroy { const adapter = new PrismaPg(this.pool); this.prisma = new PrismaClient({ adapter }); } else { + const adapter = new PrismaPg({ connectionString: dbUrl }); this.prisma = new PrismaClient({ - datasourceUrl: dbUrl, + adapter, }); } } diff --git a/apps/api/v2/src/modules/prisma/prisma-write.service.ts b/apps/api/v2/src/modules/prisma/prisma-write.service.ts index c847b2d438..91e659079c 100644 --- a/apps/api/v2/src/modules/prisma/prisma-write.service.ts +++ b/apps/api/v2/src/modules/prisma/prisma-write.service.ts @@ -60,8 +60,9 @@ export class PrismaWriteService implements OnModuleInit, OnModuleDestroy { const adapter = new PrismaPg(this.pool); this.prisma = new PrismaClient({ adapter }); } else { + const adapter = new PrismaPg({ connectionString: dbUrl }); this.prisma = new PrismaClient({ - datasourceUrl: dbUrl, + adapter, }); } } diff --git a/biome.json b/biome.json index bf4a0ffa64..72bf27d91f 100644 --- a/biome.json +++ b/biome.json @@ -164,6 +164,7 @@ { "includes": [ "apps/api/v2/src/config/app.ts", + "apps/api/v2/src/main.ts", "apps/api/v2/src/bootstrap.ts", "apps/api/v2/src/config/env.ts", "apps/api/v2/src/env.ts", @@ -325,9 +326,7 @@ } }, { - "includes": [ - "packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}" - ], + "includes": ["packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"], "linter": { "rules": { "style": { @@ -336,17 +335,11 @@ "options": { "patterns": [ { - "group": [ - "@calcom/trpc", - "@calcom/trpc/**" - ], + "group": ["@calcom/trpc", "@calcom/trpc/**"], "message": "atoms package should not import from @calcom/trpc." }, { - "group": [ - "../../trpc", - "../../trpc/**" - ], + "group": ["../../trpc", "../../trpc/**"], "message": "atoms package should not import from trpc." } ] diff --git a/packages/platform/libraries/vite.config.js b/packages/platform/libraries/vite.config.js index 2e9c06fa91..5b9a4c207e 100644 --- a/packages/platform/libraries/vite.config.js +++ b/packages/platform/libraries/vite.config.js @@ -1,19 +1,23 @@ // vite.config.ts -import react from "@vitejs/plugin-react"; -import { resolve } from "node:path"; -import path from "node:path" -import { dirname } from "node:path"; + +import path, { dirname, resolve } from "node:path"; +import process from "node:process"; import { fileURLToPath } from "node:url"; +import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import dts from "vite-plugin-dts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +const usePool = process.env.USE_POOL ?? "true"; + +console.log("Platform libraries usePool", usePool); + // https://vitejs.dev/guide/build.html#library-mode export default defineConfig({ define: { - "process.env.USE_POOL": `"true"`, + "process.env.USE_POOL": JSON.stringify(usePool), }, esbuild: { target: "node18", diff --git a/turbo.json b/turbo.json index e8cdb94061..c056404eed 100644 --- a/turbo.json +++ b/turbo.json @@ -292,6 +292,33 @@ "LINKEDIN_ADS_ENABLED", "SEED_PLATFORM_OAUTH_CLIENT_ID", "SEED_PLATFORM_OAUTH_CLIENT_SECRET", + "API_PORT", + "API_ENV", + "API_URL", + "DATABASE_WRITE_URL", + "JWT_SECRET", + "DOCS_URL", + "DATABASE_READ_URL", + "GET_LICENSE_KEY_URL", + "LOG_LEVEL", + "RATE_LIMIT_DEFAULT_TTL_MS", + "NEXTAUTH_SECRET_BACKUP", + "RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN", + "RATE_LIMIT_DEFAULT_LIMIT", + "RATE_LIMIT_DEFAULT_LIMIT_API_KEY", + "RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT", + "STRIPE_API_KEY", + "STRIPE_PRICE_ID_SCALE", + "STRIPE_PRICE_ID_ESSENTIALS_OVERAGE", + "REPLEXICA_API_KEY", + "SLOTS_WORKER_POOL_SIZE", + "STRIPE_PRICE_ID_STARTER", + "STRIPE_PRICE_ID_SCALE_OVERAGE", + "STRIPE_PRICE_ID_STARTER_OVERAGE", + "STRIPE_PRICE_ID_ESSENTIALS", + "WEB_APP_URL", + "REDIS_URL", + "ENABLE_SLOTS_WORKERS", "B2_APPLICATION_KEY_ID", "B2_APPLICATION_KEY", "B2_BUCKET_ID", @@ -382,6 +409,7 @@ }, "@calcom/api-v2#build": { "dependsOn": ["^build"], + "outputs": ["dist/**"], "env": [ "NODE_ENV", "API_PORT", diff --git a/yarn.lock b/yarn.lock index 5cbec33c58..b05b6d2616 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1935,7 +1935,7 @@ __metadata: "@microsoft/microsoft-graph-types-beta": "npm:0.42.0-preview" "@nest-lab/throttler-storage-redis": "npm:1.0.0" "@nestjs/axios": "npm:4.0.0" - "@nestjs/bull": "npm:10.1.1" + "@nestjs/bull": "npm:11.0.4" "@nestjs/cli": "npm:10.3.2" "@nestjs/common": "npm:10.4.20" "@nestjs/config": "npm:3.2.0" @@ -8209,29 +8209,29 @@ __metadata: languageName: node linkType: hard -"@nestjs/bull-shared@npm:^10.1.1": - version: 10.1.1 - resolution: "@nestjs/bull-shared@npm:10.1.1" +"@nestjs/bull-shared@npm:^11.0.4": + version: 11.0.4 + resolution: "@nestjs/bull-shared@npm:11.0.4" dependencies: - tslib: "npm:2.6.2" + tslib: "npm:2.8.1" peerDependencies: - "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 - "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 - checksum: 10/d8eb7747e6e5d0e8cc7ebd1c9824fc6a99bdf5798ab3b192a60643a4fb6bf3c352d336990b52c7437a00265724e7fb1449a57aec6e9d82bb35bdb898e2fe10c1 + "@nestjs/common": ^10.0.0 || ^11.0.0 + "@nestjs/core": ^10.0.0 || ^11.0.0 + checksum: 10/dd8c9e93805e7e44b478e39652d513bbdba8b445491c0ebc6c7e5d03c48cb08a085c9a21ffa24091215c0a03f2977162310f4218d547cc029b28c8649d801060 languageName: node linkType: hard -"@nestjs/bull@npm:10.1.1": - version: 10.1.1 - resolution: "@nestjs/bull@npm:10.1.1" +"@nestjs/bull@npm:11.0.4": + version: 11.0.4 + resolution: "@nestjs/bull@npm:11.0.4" dependencies: - "@nestjs/bull-shared": "npm:^10.1.1" - tslib: "npm:2.6.2" + "@nestjs/bull-shared": "npm:^11.0.4" + tslib: "npm:2.8.1" peerDependencies: - "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 - "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 bull: ^3.3 || ^4.0.0 - checksum: 10/8b97a3f6b12927a22dd0fcfbebeaeb978f8e783da084fd50d79b34b72570593b7ee926f94fdf8e50eca3530282227ac4abe759e0bdd555fbbf2dda4f54b5a0ae + checksum: 10/6338e53537789b0aef6a9558c3baedfef04ab96c4442a22afebfc1ac9da9951e9b49c5b1ccef3d56009b7ddf0dec59a59e17663aa01a3171eebca13e791feb58 languageName: node linkType: hard @@ -37370,13 +37370,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca - languageName: node - linkType: hard - "tslib@npm:2.8.1, tslib@npm:^2, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1"