feat: add initial project configuration and smoke tests

- Created pnpm workspace configuration to manage packages.
- Added a placeholder .gitkeep file in the scripts directory.
- Implemented a smoke test script to validate core API and web endpoints.
- Established TypeScript base configuration for consistent compilation settings.
- Introduced Turbo configuration for task management and build processes.
This commit is contained in:
KinSun
2026-03-13 10:30:16 +08:00
commit 9d5616fdc6
68 changed files with 9851 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": false,
"tsConfigPath": "tsconfig.build.json"
}
}

51
packages/api/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "@davinci/api",
"version": "0.1.0-alpha.0",
"description": "Davinci Platform — NestJS API backend (Fastify adapter)",
"author": "Galaxis",
"private": true,
"license": "UNLICENSED",
"main": "dist/main.js",
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"test": "vitest run",
"test:unit": "vitest run --config vitest.config.ts",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:cov": "vitest run --coverage",
"lint": "eslint \"{src,test}/**/*.ts\"",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
},
"dependencies": {
"@davinci/shared": "workspace:*",
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-fastify": "^11.0.0",
"@nestjs/swagger": "^11.2.6",
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.0.0",
"@scalar/fastify-api-reference": "^1.48.5",
"dotenv": "^16.4.0",
"joi": "^17.13.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/cli": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@swc/core": "^1.10.0",
"@types/node": "^22.10.0",
"pino-pretty": "^13.1.3",
"prisma": "^7.0.0",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"unplugin-swc": "^1.5.0",
"vitest": "^3.0.0"
},
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}

View File

@@ -0,0 +1,13 @@
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'npx tsx prisma/seed.ts',
},
datasource: {
url: process.env['DATABASE_URL'],
},
});

View File

@@ -0,0 +1,14 @@
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
// Placeholder model — removed when real models are added in feature plans
model Placeholder {
id String @id @default(uuid())
}

View File

@@ -0,0 +1,43 @@
/**
* Prisma seed script — creates deterministic test data for local development.
*
* Run: npx prisma db seed
* make dev-seed
*
* This is idempotent — safe to run multiple times.
*
* Replace/extend with real seed data when feature models are added.
*/
import { PrismaClient } from '../src/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env['DATABASE_URL']! });
const prisma = new PrismaClient({ adapter });
async function main(): Promise<void> {
console.log('');
console.log('🌱 Seeding davinci-platform dev database...');
console.log('');
// Placeholder — replace when real models exist
const existing = await prisma.placeholder.findFirst();
if (existing) {
console.log(' ⏭ Placeholder record already exists');
} else {
await prisma.placeholder.create({ data: {} });
console.log(' ✅ Placeholder record created');
}
console.log('');
console.log('✅ Seed complete');
console.log('');
}
main()
.catch((e) => {
console.error('❌ Seed failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { HealthModule } from './health/health.module.js';
import { PrismaModule } from './prisma/prisma.module.js';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DATABASE_URL: Joi.string().uri().required(),
PORT: Joi.number().default(3001),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
DAVINCI_MODE: Joi.string().valid('mothership', 'community').default('mothership'),
ENABLE_API_DOCS: Joi.string().valid('true', 'false', '1', '0').optional(),
}),
}),
PrismaModule,
HealthModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,52 @@
import { afterEach, describe, it, expect } from 'vitest';
import { resolveEnableApiDocs, API_VERSIONS } from '../api-docs.setup';
describe('resolveEnableApiDocs', () => {
const originalEnv = process.env.ENABLE_API_DOCS;
afterEach(() => {
if (originalEnv === undefined) {
delete process.env.ENABLE_API_DOCS;
} else {
process.env.ENABLE_API_DOCS = originalEnv;
}
});
it('returns true when ENABLE_API_DOCS=true', () => {
process.env.ENABLE_API_DOCS = 'true';
expect(resolveEnableApiDocs('production')).toBe(true);
});
it('returns true when ENABLE_API_DOCS=1', () => {
process.env.ENABLE_API_DOCS = '1';
expect(resolveEnableApiDocs('production')).toBe(true);
});
it('returns false when ENABLE_API_DOCS=false', () => {
process.env.ENABLE_API_DOCS = 'false';
expect(resolveEnableApiDocs('development')).toBe(false);
});
it('defaults to true in development when not set', () => {
delete process.env.ENABLE_API_DOCS;
expect(resolveEnableApiDocs('development')).toBe(true);
});
it('defaults to false in production when not set', () => {
delete process.env.ENABLE_API_DOCS;
expect(resolveEnableApiDocs('production')).toBe(false);
});
});
describe('API_VERSIONS', () => {
it('has at least one version defined', () => {
expect(API_VERSIONS.length).toBeGreaterThanOrEqual(1);
});
it('has v1 as the default version', () => {
const defaultVersion = API_VERSIONS.find((v) => v.default);
expect(defaultVersion).toBeDefined();
expect(defaultVersion!.version).toBe('1');
expect(defaultVersion!.slug).toBe('v1');
});
});

View File

@@ -0,0 +1,152 @@
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { ConfigService } from '@nestjs/config';
import {
DocumentBuilder,
OpenAPIObject,
SwaggerModule,
SwaggerDocumentOptions,
} from '@nestjs/swagger';
export interface ApiVersionDefinition {
version: string;
title: string;
slug: string;
default?: boolean;
documentOptions?: SwaggerDocumentOptions;
}
export const API_VERSIONS: ApiVersionDefinition[] = [
{
version: '1',
title: 'API v1',
slug: 'v1',
default: true,
},
];
/**
* Configure multi-version OpenAPI and Scalar API reference UI.
*
* Generates OpenAPI docs via @nestjs/swagger, serves per-version JSON at
* `/api-reference/{slug}/openapi.json`, and registers Scalar UI at `/api-reference`.
*
* The `/api-reference` endpoint is operational (like `/health`) and is NOT
* subject to `/api/v{major}` versioning.
*/
export async function setupApiDocs(
app: NestFastifyApplication,
configService: ConfigService,
): Promise<void> {
const enableDocs = resolveEnableApiDocs(
configService.get<string>('NODE_ENV') || process.env.NODE_ENV || 'development',
);
if (!enableDocs) {
return;
}
const versionDocs = buildVersionedDocuments(app);
registerOpenApiEndpoints(app, versionDocs);
await registerScalarReference(app, versionDocs);
}
/**
* Determine whether API docs should be enabled.
*
* 1. ENABLE_API_DOCS env var (explicit override)
* 2. Default: enabled in development/test, disabled in production
*/
export function resolveEnableApiDocs(nodeEnv: string): boolean {
const explicit = process.env.ENABLE_API_DOCS;
if (explicit !== undefined) {
return explicit === 'true' || explicit === '1';
}
return nodeEnv !== 'production';
}
export function buildVersionedDocuments(app: NestFastifyApplication): Map<string, OpenAPIObject> {
const docs = new Map<string, OpenAPIObject>();
for (const version of API_VERSIONS) {
docs.set(version.slug, buildOpenApiDocument(app, version));
}
return docs;
}
export function buildOpenApiDocument(
app: NestFastifyApplication,
version?: ApiVersionDefinition,
): OpenAPIObject {
const v = version?.version ?? '1';
const config = new DocumentBuilder()
.setTitle('Davinci Platform API')
.setDescription(
'Unified API for the Davinci platform.\n\n' +
'## Versioning\n\n' +
`All business endpoints are served under \`/api/v${v}\`. ` +
'Operational endpoints (`/health`, `/api-reference`) are unversioned.',
)
.setVersion('0.1.0')
.setContact('Davinci', '', '')
.setLicense('UNLICENSED', '')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description:
'JWT access token. Obtain via POST /api/v1/auth/login or /api/v1/auth/register.',
},
'bearer',
)
.build();
return SwaggerModule.createDocument(app, config, version?.documentOptions);
}
function registerOpenApiEndpoints(
app: NestFastifyApplication,
versionDocs: Map<string, OpenAPIObject>,
): void {
const fastify = app.getHttpAdapter().getInstance();
for (const [slug, doc] of versionDocs) {
fastify.get(
`/api-reference/${slug}/openapi.json`,
{ schema: { hide: true } as Record<string, unknown> },
async () => doc,
);
}
}
async function registerScalarReference(
app: NestFastifyApplication,
versionDocs: Map<string, OpenAPIObject>,
): Promise<void> {
const fastify = app.getHttpAdapter().getInstance();
// Dynamic import — @scalar/fastify-api-reference is ESM-only
const { default: scalarFastifyApiReference } = await import('@scalar/fastify-api-reference');
const sources = API_VERSIONS.filter((v) => versionDocs.has(v.slug)).map((v) => ({
title: v.title,
slug: v.slug,
url: `/api-reference/${v.slug}/openapi.json`,
...(v.default && { default: true }),
}));
await fastify.register(scalarFastifyApiReference, {
routePrefix: '/api-reference',
configuration: {
sources,
theme: 'kepler',
hideClientButton: true,
showDeveloperTools: 'never',
hideDownloadButton: true,
agent: { disabled: true },
} as Record<string, unknown>,
});
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { HealthController } from './health.controller.js';
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it('should return ok status with timestamp', () => {
const result = controller.check();
expect(result.status).toBe('ok');
expect(result.timestamp).toBeDefined();
expect(typeof result.timestamp).toBe('string');
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
@Controller({ path: 'health', version: VERSION_NEUTRAL })
export class HealthController {
@Get()
check() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
};
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller.js';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

57
packages/api/src/main.ts Normal file
View File

@@ -0,0 +1,57 @@
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { VersioningType, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module.js';
import { setupApiDocs } from './docs/api-docs.setup.js';
async function bootstrap() {
const logger = new Logger('Bootstrap');
const isDev = process.env.NODE_ENV !== 'production';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: {
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
...(isDev && {
transport: {
target: 'pino-pretty',
options: { colorize: true, singleLine: true },
},
}),
},
}),
);
// Global prefix — all routes under /api, except operational endpoints
app.setGlobalPrefix('api', {
exclude: ['health', 'health/ready', 'api-reference', 'api-reference/{*path}'],
});
// URI-based versioning: /api/v1/..., /api/v2/...
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// CORS — env-driven: permissive in dev, strict in production
const corsOrigin = process.env.CORS_ORIGIN;
app.enableCors({
origin: corsOrigin ? corsOrigin.split(',').map((o) => o.trim()) : true,
credentials: true,
});
// Interactive API documentation (Scalar) — conditional on ENABLE_API_DOCS
const configService = app.get(ConfigService);
await setupApiDocs(app, configService);
// Graceful shutdown
app.enableShutdownHooks();
const port = process.env.PORT ?? 3001;
await app.listen(port, '0.0.0.0');
logger.log(`API running on http://localhost:${port}`);
}
bootstrap();

View File

@@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { PrismaService } from '../prisma.service.js';
import { ConfigService } from '@nestjs/config';
describe('PrismaService', () => {
let service: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PrismaService,
{
provide: ConfigService,
useValue: {
getOrThrow: vi.fn().mockReturnValue('postgresql://test:test@localhost:5432/test'),
},
},
],
}).compile();
service = module.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should expose PrismaClient methods directly (extends pattern)', () => {
expect(typeof service.$connect).toBe('function');
expect(typeof service.$disconnect).toBe('function');
});
});

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service.js';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,33 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '../generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
/**
* NestJS-managed Prisma service for Prisma 7.
*
* Follows the official NestJS + Prisma 7 recipe:
* - Uses `prisma-client` generator with `moduleFormat = "cjs"`
* - Extends PrismaClient directly for full type-safe API access
* - Uses PrismaPg driver adapter with connectionString
*
* Usage in feature modules:
* constructor(private readonly prisma: PrismaService) {}
* await this.prisma.placeholder.findMany();
*/
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor(configService: ConfigService) {
const databaseUrl = configService.getOrThrow<string>('DATABASE_URL');
const adapter = new PrismaPg({ connectionString: databaseUrl });
super({ adapter });
}
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "node_modules", "**/*.spec.ts", "**/*.test.ts", "test"]
}

View File

@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"paths": {
"@davinci/shared": ["../shared/src/index.ts"]
}
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,18 @@
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
exclude: ['src/**/*.e2e-spec.ts'],
},
plugins: [swc.vite()],
resolve: {
alias: {
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
},
},
});

View File

@@ -0,0 +1,17 @@
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['test/**/*.e2e-spec.ts', 'src/**/*.e2e-spec.ts'],
},
plugins: [swc.vite()],
resolve: {
alias: {
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
},
},
});