Skip to content

Commit 9ad8aa9

Browse files
ThyMinimalDevdevin-ai-integration[bot]cubic-dev-ai[bot]
authored
chore: deploy api v2 on vercel (#26735)
* chore: deploy api v2 on vercel * fix: replace console.log with logger.log in Vercel handler Address Cubic AI review feedback to use the logging framework consistently instead of console.log in the serverless handler. Co-Authored-By: unknown <> * chore: enable esModuleInterop * chore: deploy api v2 on vercel * chore: deploy api v2 on vercel * Update apps/api/v2/src/bootstrap.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fixup! Merge branch 'main' into deploy-api-v2-vercel * Revert "chore: deploy api v2 on vercel" This reverts commit 45c704a. * chore: deploy api v2 on vercel * fix: address Cubic AI review feedback in main.ts - Replace console.log with logger.log for consistent logging - Replace console.error with logger.error for consistent error logging - Restore comma: true option in qs.parse to support comma-separated arrays Co-Authored-By: unknown <> * fix: remove comma: true from qs.parse to maintain backward compatibility The main branch does not have comma: true in the query parser, so adding it would be a breaking change for existing API consumers. Removing it to maintain consistency with the current production behavior. Co-Authored-By: unknown <> * chore: deploy api v2 on vercel * small fixes * chore: add try catch around bootstrap.ts * fix: use NestJS Logger and throw error instead of process.exit in bootstrap - Replace console.error with logger.error for consistent logging - Replace process.exit(1) with throw error to avoid breaking Vercel serverless instance reuse Addresses Cubic AI review feedback (confidence 10/10 for both issues) Co-Authored-By: unknown <> * chore: try log redis url * fix: sanitize REDIS_URL logging to avoid exposing credentials Replace full REDIS_URL logging with a boolean check that only indicates whether Redis is configured, without exposing the connection string. Addresses Cubic AI review feedback (confidence 9/10) Co-Authored-By: unknown <> * chore: remove unnecessary logs * fix: prisma adapter * chore: handle USE_POOL platform libraries * fix: use JSON.stringify for Vite define value Wrap usePool with JSON.stringify() to properly serialize the string value. Without this, Vite injects the raw value as an identifier instead of a string literal, breaking runtime behavior. Addresses Cubic AI review feedback (confidence 9/10) Co-Authored-By: unknown <> * fix: docker file builds * fix: correct Dockerfile build order for platform packages Reorder builds to match the dependency graph from dev:build script: constants → enums → utils → types → libraries → trpc → api-v2 platform-libraries depends on the other platform packages, so they must be built first. Addresses Cubic AI review feedback (confidence 9/10) Co-Authored-By: unknown <> * fix: docker file builds * chore: add docker build * chore: upgrade nest/bull --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent bb9581d commit 9ad8aa9

File tree

10 files changed

+202
-109
lines changed

10 files changed

+202
-109
lines changed

apps/api/v2/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ ENV NODE_ENV="production"
1212
ENV NODE_OPTIONS="--max-old-space-size=8192"
1313
ENV DATABASE_DIRECT_URL=${DATABASE_DIRECT_URL}
1414
ENV DATABASE_URL=${DATABASE_URL}
15+
ENV USE_POOL="true"
1516

1617
COPY . .
1718

1819
RUN yarn install
1920
RUN yarn workspace @calcom/api-v2 run generate-schemas
20-
RUN yarn workspace @calcom/platform-libraries run build
21+
RUN yarn workspace @calcom/api-v2 run build:docker
2122
RUN yarn workspace @calcom/api-v2 run build
2223

2324
EXPOSE 80

apps/api/v2/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"license": "UNLICENSED",
77
"private": true,
88
"scripts": {
9-
"build": "yarn dev:build && nest build",
9+
"build": "nest build",
10+
"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",
1011
"format": "biome format --write src test",
1112
"start": "nest start",
1213
"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 @@
4748
"@microsoft/microsoft-graph-types-beta": "0.42.0-preview",
4849
"@nest-lab/throttler-storage-redis": "1.0.0",
4950
"@nestjs/axios": "4.0.0",
50-
"@nestjs/bull": "10.1.1",
51+
"@nestjs/bull": "11.0.4",
5152
"@nestjs/common": "10.4.20",
5253
"@nestjs/config": "3.2.0",
5354
"@nestjs/core": "10.4.20",

apps/api/v2/src/bootstrap.ts

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
X_CAL_SECRET_KEY,
1111
} from "@calcom/platform-constants";
1212
import type { ValidationError } from "@nestjs/common";
13-
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
13+
import { BadRequestException, Logger, ValidationPipe, VersioningType } from "@nestjs/common";
1414
import type { NestExpressApplication } from "@nestjs/platform-express";
1515
import cookieParser from "cookie-parser";
1616
import { Request } from "express";
@@ -21,62 +21,68 @@ import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
2121
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
2222
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
2323

24-
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
25-
app.enableShutdownHooks();
26-
app.enableVersioning({
27-
type: VersioningType.CUSTOM,
28-
extractor: (request: unknown) => {
29-
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
30-
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
31-
return headerVersion;
32-
}
33-
return VERSION_2024_04_15;
34-
},
35-
defaultVersion: VERSION_2024_04_15,
36-
});
37-
app.use(helmet());
38-
app.enableCors({
39-
origin: "*",
40-
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
41-
allowedHeaders: [
42-
X_CAL_CLIENT_ID,
43-
X_CAL_SECRET_KEY,
44-
X_CAL_PLATFORM_EMBED,
45-
CAL_API_VERSION_HEADER,
46-
"Accept",
47-
"Authorization",
48-
"Content-Type",
49-
"Origin",
50-
],
51-
maxAge: 86_400,
52-
});
24+
const logger: Logger = new Logger("Bootstrap");
5325

54-
app.useGlobalPipes(
55-
new ValidationPipe({
56-
whitelist: true,
57-
transform: true,
58-
validationError: {
59-
target: true,
60-
value: true,
61-
},
62-
exceptionFactory(errors: ValidationError[]): BadRequestException {
63-
return new BadRequestException({ errors });
26+
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
27+
try {
28+
if (!process.env.VERCEL) {
29+
app.enableShutdownHooks();
30+
}
31+
app.enableVersioning({
32+
type: VersioningType.CUSTOM,
33+
extractor: (request: unknown) => {
34+
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
35+
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
36+
return headerVersion;
37+
}
38+
return VERSION_2024_04_15;
6439
},
65-
})
66-
);
40+
defaultVersion: VERSION_2024_04_15,
41+
});
42+
app.use(helmet());
43+
app.enableCors({
44+
origin: "*",
45+
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
46+
allowedHeaders: [
47+
X_CAL_CLIENT_ID,
48+
X_CAL_SECRET_KEY,
49+
X_CAL_PLATFORM_EMBED,
50+
CAL_API_VERSION_HEADER,
51+
"Accept",
52+
"Authorization",
53+
"Content-Type",
54+
"Origin",
55+
],
56+
maxAge: 86_400,
57+
});
58+
app.useGlobalPipes(
59+
new ValidationPipe({
60+
whitelist: true,
61+
transform: true,
62+
validationError: {
63+
target: true,
64+
value: true,
65+
},
66+
exceptionFactory(errors: ValidationError[]): BadRequestException {
67+
return new BadRequestException({ errors });
68+
},
69+
})
70+
);
71+
// Exception filters, new filters go at the bottom, keep the order
72+
app.useGlobalFilters(new PrismaExceptionFilter());
73+
app.useGlobalFilters(new ZodExceptionFilter());
74+
app.useGlobalFilters(new HttpExceptionFilter());
75+
app.useGlobalFilters(new TRPCExceptionFilter());
76+
app.useGlobalFilters(new CalendarServiceExceptionFilter());
77+
app.use(cookieParser());
6778

68-
// Exception filters, new filters go at the bottom, keep the order
69-
app.useGlobalFilters(new PrismaExceptionFilter());
70-
app.useGlobalFilters(new ZodExceptionFilter());
71-
app.useGlobalFilters(new HttpExceptionFilter());
72-
app.useGlobalFilters(new TRPCExceptionFilter());
73-
app.useGlobalFilters(new CalendarServiceExceptionFilter());
79+
if (process?.env?.API_GLOBAL_PREFIX) {
80+
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
81+
}
7482

75-
app.use(cookieParser());
76-
77-
if (process?.env?.API_GLOBAL_PREFIX) {
78-
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
83+
return app;
84+
} catch (error) {
85+
logger.error("Error starting NestJS app:", error);
86+
throw error;
7987
}
80-
81-
return app;
8288
};

apps/api/v2/src/main.ts

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
import "dotenv/config";
22

33
import { IncomingMessage, Server, ServerResponse } from "node:http";
4+
45
import { Logger } from "@nestjs/common";
56
import { ConfigService } from "@nestjs/config";
67
import { NestFactory } from "@nestjs/core";
78
import type { NestExpressApplication } from "@nestjs/platform-express";
9+
import type { Express, Request, Response } from "express";
810
import { WinstonModule } from "nest-winston";
911
import qs from "qs";
1012
import type { AppConfig } from "@/config/type";
13+
1114
import { AppModule } from "./app.module";
1215
import { bootstrap } from "./bootstrap";
1316
import { loggerConfig } from "./lib/logger";
1417

15-
run().catch((error: Error) => {
16-
console.error("Failed to start Cal Platform API", { error: error.stack });
17-
process.exit(1);
18-
});
18+
const logger: Logger = new Logger("App");
19+
20+
/**
21+
* Singleton Class to manage the NestJS App instance.
22+
* Ensures we only initialize the app once per container lifecycle.
23+
*/
24+
class NestServer {
25+
private static server: Express; // The underlying Express instance
26+
27+
private constructor() {}
28+
29+
/**
30+
* Returns the cached server instance.
31+
* If it doesn't exist, it creates, bootstraps, and initializes it.
32+
*/
33+
public static async getInstance(): Promise<Express> {
34+
if (!NestServer.server) {
35+
const app = await createNestApp();
36+
37+
// Execute bootstrap (Pipes, Interceptors, CORS, etc.)
38+
bootstrap(app);
39+
40+
// Initialize the app (connects to DB, resolves modules)
41+
await app.init();
42+
43+
// extract the Express instance to pass to Vercel
44+
NestServer.server = app.getHttpAdapter().getInstance();
45+
}
46+
return NestServer.server;
47+
}
48+
}
49+
50+
// -----------------------------------------------------------------------------
51+
// LOCAL DEVELOPMENT STARTUP
52+
// -----------------------------------------------------------------------------
53+
if (!process.env.VERCEL) {
54+
run().catch((error: Error) => {
55+
logger.error("Failed to start Cal Platform API", { error: error.stack });
56+
process.exit(1);
57+
});
58+
}
1959

2060
async function run(): Promise<void> {
2161
const app = await createNestApp();
22-
const logger = new Logger("App");
23-
2462
try {
2563
bootstrap(app);
2664
const config = app.get(ConfigService<AppConfig, true>);
@@ -32,23 +70,50 @@ async function run(): Promise<void> {
3270
}
3371

3472
await app.listen(port);
35-
logger.log(`Application started on port: ${port}`);
73+
logger.log(`Application started locally on port: ${port}`);
3674
} catch (error) {
37-
console.error(error);
38-
logger.error("Application crashed", {
39-
error,
40-
});
75+
logger.error("Application crashed during local startup", { error });
4176
}
4277
}
4378

79+
// -----------------------------------------------------------------------------
80+
// VERCEL SERVERLESS HANDLER
81+
// -----------------------------------------------------------------------------
82+
export default async (req: Request, res: Response): Promise<void> => {
83+
try {
84+
const server = await NestServer.getInstance();
85+
86+
// Vercel/AWS specific: Re-parse query strings to support array formats
87+
// (e.g., ?ids[]=1&ids[]=2) which Vercel's native parser might simplify.
88+
if (req.url) {
89+
const [_path, queryString] = req.url.split("?");
90+
if (queryString) {
91+
req.query = qs.parse(queryString, { arrayLimit: 1000 });
92+
}
93+
}
94+
95+
// Delegate request to the cached Express instance
96+
return server(req, res);
97+
} catch (error) {
98+
logger.error("Critical: Failed to initialize NestJS Serverless instance", error);
99+
res.statusCode = 500;
100+
res.end("Internal Server Error: Initialization Failed");
101+
}
102+
};
103+
104+
// -----------------------------------------------------------------------------
105+
// APP FACTORY
106+
// -----------------------------------------------------------------------------
44107
export async function createNestApp(): Promise<
45108
NestExpressApplication<Server<typeof IncomingMessage, typeof ServerResponse>>
46109
> {
47110
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
48111
logger: WinstonModule.createLogger(loggerConfig()),
112+
// Preserved as requested:
49113
bodyParser: false,
50114
});
51115

116+
// Custom query parser configuration for the underlying Express app
52117
app.set("query parser", (str: string) => qs.parse(str, { arrayLimit: 1000 }));
53118

54119
return app;

apps/api/v2/src/modules/prisma/prisma-read.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
5454
const adapter = new PrismaPg(this.pool);
5555
this.prisma = new PrismaClient({ adapter });
5656
} else {
57+
const adapter = new PrismaPg({ connectionString: dbUrl });
5758
this.prisma = new PrismaClient({
58-
datasourceUrl: dbUrl,
59+
adapter,
5960
});
6061
}
6162
}

apps/api/v2/src/modules/prisma/prisma-write.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
6060
const adapter = new PrismaPg(this.pool);
6161
this.prisma = new PrismaClient({ adapter });
6262
} else {
63+
const adapter = new PrismaPg({ connectionString: dbUrl });
6364
this.prisma = new PrismaClient({
64-
datasourceUrl: dbUrl,
65+
adapter,
6566
});
6667
}
6768
}

biome.json

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
{
165165
"includes": [
166166
"apps/api/v2/src/config/app.ts",
167+
"apps/api/v2/src/main.ts",
167168
"apps/api/v2/src/bootstrap.ts",
168169
"apps/api/v2/src/config/env.ts",
169170
"apps/api/v2/src/env.ts",
@@ -325,9 +326,7 @@
325326
}
326327
},
327328
{
328-
"includes": [
329-
"packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"
330-
],
329+
"includes": ["packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"],
331330
"linter": {
332331
"rules": {
333332
"style": {
@@ -336,17 +335,11 @@
336335
"options": {
337336
"patterns": [
338337
{
339-
"group": [
340-
"@calcom/trpc",
341-
"@calcom/trpc/**"
342-
],
338+
"group": ["@calcom/trpc", "@calcom/trpc/**"],
343339
"message": "atoms package should not import from @calcom/trpc."
344340
},
345341
{
346-
"group": [
347-
"../../trpc",
348-
"../../trpc/**"
349-
],
342+
"group": ["../../trpc", "../../trpc/**"],
350343
"message": "atoms package should not import from trpc."
351344
}
352345
]

packages/platform/libraries/vite.config.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
// vite.config.ts
2-
import react from "@vitejs/plugin-react";
3-
import { resolve } from "node:path";
4-
import path from "node:path"
5-
import { dirname } from "node:path";
2+
3+
import path, { dirname, resolve } from "node:path";
4+
import process from "node:process";
65
import { fileURLToPath } from "node:url";
6+
import react from "@vitejs/plugin-react";
77
import { defineConfig } from "vite";
88
import dts from "vite-plugin-dts";
99

1010
const __filename = fileURLToPath(import.meta.url);
1111
const __dirname = dirname(__filename);
1212

13+
const usePool = process.env.USE_POOL ?? "true";
14+
15+
console.log("Platform libraries usePool", usePool);
16+
1317
// https://vitejs.dev/guide/build.html#library-mode
1418
export default defineConfig({
1519
define: {
16-
"process.env.USE_POOL": `"true"`,
20+
"process.env.USE_POOL": JSON.stringify(usePool),
1721
},
1822
esbuild: {
1923
target: "node18",

0 commit comments

Comments
 (0)