diff --git a/apps/server/src/http-body-limit.spec.ts b/apps/server/src/http-body-limit.spec.ts new file mode 100644 index 0000000..3aef644 --- /dev/null +++ b/apps/server/src/http-body-limit.spec.ts @@ -0,0 +1,38 @@ +import { configureHttpBodyParsers, DEFAULT_HTTP_BODY_LIMIT } from './http-body-limit'; + +jest.mock('express', () => ({ + json: jest.fn(() => 'json-parser'), + urlencoded: jest.fn(() => 'urlencoded-parser'), +})); + +const express = jest.requireMock('express') as { + json: jest.Mock; + urlencoded: jest.Mock; +}; + +describe('configureHttpBodyParsers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('registers JSON and urlencoded parsers with a large AppData-safe limit', () => { + const app = { use: jest.fn() }; + + configureHttpBodyParsers(app as never); + + expect(DEFAULT_HTTP_BODY_LIMIT).toBe('10mb'); + expect(express.json).toHaveBeenCalledWith({ limit: '10mb' }); + expect(express.urlencoded).toHaveBeenCalledWith({ extended: true, limit: '10mb' }); + expect(app.use).toHaveBeenNthCalledWith(1, 'json-parser'); + expect(app.use).toHaveBeenNthCalledWith(2, 'urlencoded-parser'); + }); + + it('allows overriding the body limit for deployments with larger documents', () => { + const app = { use: jest.fn() }; + + configureHttpBodyParsers(app as never, '25mb'); + + expect(express.json).toHaveBeenCalledWith({ limit: '25mb' }); + expect(express.urlencoded).toHaveBeenCalledWith({ extended: true, limit: '25mb' }); + }); +}); diff --git a/apps/server/src/http-body-limit.ts b/apps/server/src/http-body-limit.ts new file mode 100644 index 0000000..83288ef --- /dev/null +++ b/apps/server/src/http-body-limit.ts @@ -0,0 +1,12 @@ +import type { INestApplication } from '@nestjs/common'; +import { json, urlencoded } from 'express'; + +export const DEFAULT_HTTP_BODY_LIMIT = '10mb'; + +export function configureHttpBodyParsers( + app: Pick, + limit = process.env.HTTP_BODY_LIMIT || DEFAULT_HTTP_BODY_LIMIT, +) { + app.use(json({ limit })); + app.use(urlencoded({ extended: true, limit })); +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 7ce767a..f5a7778 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -3,6 +3,7 @@ import { ValidationPipe } from '@nestjs/common'; import { existsSync } from 'fs'; import { resolve } from 'path'; import { AppModule } from './app.module'; +import { configureHttpBodyParsers } from './http-body-limit'; // 加载 .env(Node v20+ 内置 loadEnvFile) // 优先 cwd/.env,其次 dist 上一级(编译运行时 cwd 可能在 dist/) @@ -14,7 +15,8 @@ for (const candidate of [resolve(process.cwd(), '.env'), resolve(__dirname, '../ } async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bodyParser: false }); + configureHttpBodyParsers(app); app.setGlobalPrefix('api/v1'); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); app.enableCors(); diff --git a/apps/web/lib/derive.test.ts b/apps/web/lib/derive.test.ts new file mode 100644 index 0000000..a799ce1 --- /dev/null +++ b/apps/web/lib/derive.test.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { flattenProjects, flattenVersions } from './derive'; + +const overview = [{ + id: 'product-1', + name: 'Product', + projects: [{ + id: 'project-1', + name: 'Project', + description: '', + createdAt: '2026-07-02T00:00:00.000Z', + }], + versions: [{ + id: 'version-1', + name: 'ProjectV1.0', + releaseDate: null, + createdAt: '2026-07-02T00:00:00.000Z', + }], +}]; + +test('flattenVersions defaults missing version status to planned', () => { + const [version] = flattenVersions(overview); + + assert.equal(version.status, 'planned'); +}); + +test('flattenProjects defaults nested missing version status to planned', () => { + const [project] = flattenProjects(overview); + + assert.equal(project.versions[0].status, 'planned'); +}); diff --git a/apps/web/lib/derive.ts b/apps/web/lib/derive.ts index 87020d8..8d8264a 100644 --- a/apps/web/lib/derive.ts +++ b/apps/web/lib/derive.ts @@ -71,7 +71,7 @@ export function flattenProjects(overview: ProductOverviewLike[]): ProjectWithCon .filter((v) => v.name.toLowerCase().startsWith(project.name.toLowerCase())) .map((v) => ({ ...v, - status: (v.status || 'released') as VersionStatus, + status: (v.status || 'planned') as VersionStatus, productId: product.id, productName: product.name, projectId: project.id, @@ -98,7 +98,7 @@ export function flattenVersions(overview: ProductOverviewLike[]): VersionWithCon ); result.push({ ...version, - status: (version.status || 'released') as VersionStatus, + status: (version.status || 'planned') as VersionStatus, productId: product.id, productName: product.name, projectId: project?.id || '',