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:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.git
|
||||
dist
|
||||
.next
|
||||
.env
|
||||
coverage
|
||||
.turbo
|
||||
*.md
|
||||
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev
|
||||
|
||||
# Server
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Mode: "mothership" (default) or "community"
|
||||
DAVINCI_MODE=mothership
|
||||
|
||||
# Auth (placeholder — configured in auth feature plan)
|
||||
JWT_SECRET=change-me-in-production
|
||||
|
||||
# Frontend
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
.next/
|
||||
|
||||
# Turborepo cache
|
||||
.turbo/
|
||||
|
||||
# TypeScript incremental
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# Prisma
|
||||
prisma/*.db
|
||||
generated/
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
dist
|
||||
.next
|
||||
node_modules
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
.turbo
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.0-alpha.0] — 2026-03-12
|
||||
|
||||
### Added
|
||||
|
||||
- **Monorepo scaffold** — pnpm workspaces with `packages/api`, `packages/web`, `packages/shared`
|
||||
- **NestJS 11 API shell** — Fastify 5 adapter, health endpoint (`GET /api/health`), ConfigModule with env validation
|
||||
- **Next.js 15.5.x frontend shell** — App Router, next-intl i18n, Tailwind CSS v4, shadcn/ui base config
|
||||
- **Prisma 7 schema shell** — PostgreSQL datasource, placeholder model for client generation
|
||||
- **Shared types package** — `@davinci/shared` with barrel exports, build via TypeScript
|
||||
- **Turborepo** — build caching and task orchestration across packages
|
||||
- **Vitest** — test runner for all packages (replaces Jest)
|
||||
- **ESLint flat config** — TypeScript + Prettier integration, single root config
|
||||
- **Prettier** — consistent formatting with pre-commit hooks (Husky + lint-staged)
|
||||
- **Docker** — multi-stage Dockerfile (nginx + NestJS + Next.js), docker-compose for dev
|
||||
- **nginx** — internal routing `/api/*` → :3001, `/*` → :3000
|
||||
- **Makefile** — developer command interface matching architecture spec
|
||||
- **README** — quickstart, project structure, environment variables
|
||||
- **smoke-test.sh SKIP_WEB** — `SKIP_WEB=1 make smoke` skips web container for API-only workflows
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Docker healthcheck IPv6** — changed `localhost` to `127.0.0.1` in docker-compose healthcheck URLs (Alpine wget resolves localhost → ::1 IPv6, Fastify listens on IPv4 only)
|
||||
- **Scalar ESM import (TS1479)** — converted static import of `@scalar/fastify-api-reference` to dynamic `import()` since the package is ESM-only
|
||||
- **Test import** — added missing `afterEach` to vitest import in `api-docs.setup.spec.ts`
|
||||
83
Dockerfile
Normal file
83
Dockerfile
Normal file
@@ -0,0 +1,83 @@
|
||||
# --- Stage 1: Dependencies ---
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
RUN apk add --no-cache openssl python3 make g++
|
||||
RUN corepack enable && corepack prepare pnpm@10.32.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY packages/api/package.json packages/api/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
COPY packages/web/package.json packages/web/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# --- Stage 2: Build API ---
|
||||
FROM deps AS api-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY tsconfig.base.json ./
|
||||
COPY packages/shared/ packages/shared/
|
||||
RUN pnpm --filter @davinci/shared build
|
||||
|
||||
COPY packages/api/tsconfig.json packages/api/tsconfig.build.json packages/api/nest-cli.json packages/api/prisma.config.ts packages/api/
|
||||
COPY packages/api/prisma/ packages/api/prisma/
|
||||
COPY packages/api/src/ packages/api/src/
|
||||
RUN pnpm --filter @davinci/api prisma:generate
|
||||
RUN pnpm --filter @davinci/api build
|
||||
|
||||
# --- Stage 3: Build Web ---
|
||||
FROM deps AS web-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY tsconfig.base.json ./
|
||||
COPY packages/shared/ packages/shared/
|
||||
RUN pnpm --filter @davinci/shared build
|
||||
|
||||
COPY packages/web/ packages/web/
|
||||
RUN pnpm --filter @davinci/web build
|
||||
|
||||
# --- Stage 4: Runner ---
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
RUN apk add --no-cache openssl nginx
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 appuser
|
||||
|
||||
# Copy nginx config
|
||||
COPY nginx.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
# Copy API build
|
||||
COPY --from=api-builder /app/packages/api/dist packages/api/dist
|
||||
COPY --from=api-builder /app/packages/api/prisma packages/api/prisma
|
||||
COPY --from=api-builder /app/packages/api/node_modules packages/api/node_modules
|
||||
COPY --from=api-builder /app/packages/api/package.json packages/api/
|
||||
COPY --from=api-builder /app/node_modules ./node_modules
|
||||
COPY --from=api-builder /app/package.json ./
|
||||
|
||||
# Copy Web standalone build
|
||||
COPY --from=web-builder /app/packages/web/.next/standalone packages/web/standalone
|
||||
COPY --from=web-builder /app/packages/web/.next/static packages/web/standalone/packages/web/.next/static
|
||||
COPY --from=web-builder /app/packages/web/public packages/web/standalone/packages/web/public
|
||||
|
||||
# Copy entrypoint
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# nginx needs write access to certain dirs
|
||||
RUN mkdir -p /run/nginx && chown -R appuser:nodejs /run/nginx
|
||||
RUN chown -R appuser:nodejs /var/log/nginx /var/lib/nginx
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
USER appuser
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
138
Makefile
Normal file
138
Makefile
Normal file
@@ -0,0 +1,138 @@
|
||||
# Davinci Platform — Developer Commands
|
||||
#
|
||||
# Unified monorepo: packages/api (NestJS), packages/web (Next.js), packages/shared.
|
||||
#
|
||||
# Dev stack: persistent data, ports 4300 (api) / 4000 (web) / 55434 (pg)
|
||||
# Test stack: ephemeral (always fresh DB), ports 4301 (api) / 4001 (web) / 45434 (pg)
|
||||
#
|
||||
# Usage:
|
||||
# make help Show all commands
|
||||
# make dev Start full dev stack
|
||||
# make test Full isolated smoke test
|
||||
|
||||
.PHONY: dev dev-build dev-logs dev-stop dev-reset dev-seed build test test-up test-down test\:unit test\:e2e test\:cov lint format format\:check db\:migrate db\:studio db\:push docker\:build docker\:up smoke down help
|
||||
|
||||
TEST = docker compose -p davinci-platform-test -f docker-compose.test.yml
|
||||
|
||||
# ─── Dev Stack ────────────────────────────────────────────────────
|
||||
|
||||
dev: ## Start docker-compose postgres + API + web in watch mode
|
||||
docker compose up db -d --wait
|
||||
pnpm dev
|
||||
@echo ""
|
||||
@echo " API: http://localhost:4300"
|
||||
@echo " Web: http://localhost:4000"
|
||||
@echo " Postgres: localhost:55434"
|
||||
@echo ""
|
||||
|
||||
dev-build: ## Rebuild and start dev stack (docker)
|
||||
docker compose up -d --build
|
||||
|
||||
dev-logs: ## Tail API container logs
|
||||
docker compose logs -f api
|
||||
|
||||
dev-stop: ## Stop dev containers (data preserved)
|
||||
docker compose down
|
||||
|
||||
dev-reset: ## Stop, destroy data, restart fresh
|
||||
docker compose down -v
|
||||
docker compose up -d --build
|
||||
@echo ""
|
||||
@echo " Dev stack reset with fresh database"
|
||||
@echo ""
|
||||
|
||||
dev-seed: ## Seed dev database with test data
|
||||
@echo "Seeding dev database..."
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma db seed
|
||||
@echo "Done."
|
||||
|
||||
# ─── Build ────────────────────────────────────────────────────────
|
||||
|
||||
build: ## Build all packages
|
||||
pnpm build
|
||||
|
||||
# ─── Testing ──────────────────────────────────────────────────────
|
||||
|
||||
test\:unit: ## Run unit tests only
|
||||
pnpm test:unit
|
||||
|
||||
test\:e2e: ## Run e2e tests only
|
||||
pnpm test:e2e
|
||||
|
||||
test\:cov: ## Run tests with coverage report
|
||||
pnpm test:cov
|
||||
|
||||
# ─── Test Stack (Isolated) ────────────────────────────────────────
|
||||
|
||||
test-up: ## Start isolated test environment (fresh DB, ephemeral)
|
||||
$(TEST) down -v 2>/dev/null || true
|
||||
$(TEST) up -d --build
|
||||
@echo ""
|
||||
@echo " Test API: http://localhost:4301"
|
||||
@echo " Test Web: http://localhost:4001"
|
||||
@echo " Test DB: localhost:45434"
|
||||
@echo ""
|
||||
|
||||
test-down: ## Tear down test environment (destroy data)
|
||||
$(TEST) down -v 2>/dev/null || true
|
||||
|
||||
test: test-up ## Run smoke tests in isolated test environment
|
||||
@echo "Waiting for services to be ready..."
|
||||
@sleep 10
|
||||
@API_PORT=4301 WEB_PORT=4001 bash smoke-test.sh; \
|
||||
EXIT_CODE=$$?; \
|
||||
$(MAKE) test-down; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
# ─── Smoke (against running dev stack) ────────────────────────────
|
||||
|
||||
smoke: ## Run smoke tests against dev stack (must be running)
|
||||
bash smoke-test.sh
|
||||
|
||||
# ─── Code Quality ─────────────────────────────────────────────────
|
||||
|
||||
lint: ## Lint all packages
|
||||
pnpm lint
|
||||
|
||||
format: ## Format all files with Prettier
|
||||
pnpm format
|
||||
|
||||
format\:check: ## Check formatting without writing
|
||||
pnpm format:check
|
||||
|
||||
# ─── Database ─────────────────────────────────────────────────────
|
||||
|
||||
db\:migrate: ## Create a new Prisma migration after schema changes
|
||||
@read -p "Migration name: " name; \
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma migrate dev --name $$name
|
||||
|
||||
db\:studio: ## Open Prisma Studio
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma studio
|
||||
|
||||
db\:push: ## Push schema changes (no migration — quick prototyping only)
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma db push
|
||||
|
||||
# ─── Docker ───────────────────────────────────────────────────────
|
||||
|
||||
docker\:build: ## Build production Docker image
|
||||
docker build -t davinci-platform .
|
||||
|
||||
docker\:up: ## Run full stack via docker-compose
|
||||
docker compose up -d
|
||||
|
||||
# ─── Cleanup ──────────────────────────────────────────────────────
|
||||
|
||||
down: ## Stop everything (dev + test)
|
||||
docker compose down 2>/dev/null || true
|
||||
$(TEST) down -v 2>/dev/null || true
|
||||
|
||||
# ─── Help ─────────────────────────────────────────────────────────
|
||||
|
||||
help: ## Show this help
|
||||
@grep -E '^[a-zA-Z_\\:-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
338
README.md
Normal file
338
README.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# Davinci Platform
|
||||
|
||||
Unified monorepo for the Davinci platform — replaces `davinci-mothership` and `davinci-community-server` as a single codebase with a `DAVINCI_MODE` flag.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker Desktop** (includes Docker Compose v2)
|
||||
- **Node.js** 22+ (see `.nvmrc`)
|
||||
- **pnpm** 10+ (`corepack enable && corepack prepare pnpm@10.32.0 --activate`)
|
||||
- **make** (preinstalled on macOS/Linux)
|
||||
|
||||
> Two development modes are supported:
|
||||
> 1. **Docker (recommended)** — everything runs in containers. Zero local setup beyond Docker + Node.js.
|
||||
> 2. **Local + Docker infra** — run the NestJS API and Next.js frontend natively for faster iteration, Docker for Postgres only. See [Local Development](#local-development-without-docker-for-the-app).
|
||||
|
||||
## Quick Start (Docker — Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Install local deps (for Prisma CLI, tests, IDE support)
|
||||
pnpm install
|
||||
|
||||
# 2. Copy environment config
|
||||
cp .env.example .env
|
||||
|
||||
# 3. Start the full dev environment (Postgres + API + Web)
|
||||
make dev-build
|
||||
|
||||
# 4. Run migrations
|
||||
make db:push
|
||||
|
||||
# 5. Seed the dev database with test data
|
||||
make dev-seed
|
||||
|
||||
# 6. Verify everything works
|
||||
make smoke
|
||||
```
|
||||
|
||||
**That's it.** You now have:
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| API | http://localhost:4300/health |
|
||||
| API Docs (Scalar) | http://localhost:4300/api-reference |
|
||||
| Web | http://localhost:4000 |
|
||||
| Postgres | localhost:55434 |
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
make help # Show all available commands
|
||||
make dev-logs # Tail API container logs
|
||||
make dev-stop # Stop containers (data preserved)
|
||||
make dev-reset # Destroy + rebuild from scratch
|
||||
make dev-seed # Re-seed database
|
||||
```
|
||||
|
||||
## Local Development (Without Docker for the App)
|
||||
|
||||
If you prefer running the NestJS API and Next.js frontend natively (for faster restarts, debugger attach, HMR, etc.) while still using Docker for infrastructure:
|
||||
|
||||
### 1. Start Infrastructure Only
|
||||
|
||||
```bash
|
||||
# Start only Postgres (no app containers)
|
||||
docker compose up db -d --wait
|
||||
```
|
||||
|
||||
### 2. Set Up Environment
|
||||
|
||||
Create a `.env` file (git-ignored) in the repo root:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DATABASE_URL="postgresql://davinci:davinci@localhost:55434/davinci_platform_dev"
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
DAVINCI_MODE=mothership
|
||||
ENABLE_API_DOCS=true
|
||||
```
|
||||
|
||||
### 3. Generate Prisma Client & Push Schema
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api prisma:generate
|
||||
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma db push
|
||||
```
|
||||
|
||||
### 4. Start the Apps
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start API (watch mode — auto-restarts on file changes)
|
||||
pnpm --filter @davinci/api start:dev
|
||||
|
||||
# Terminal 2: Start Web (Next.js dev server with HMR)
|
||||
pnpm --filter @davinci/web dev
|
||||
```
|
||||
|
||||
The API listens on `http://localhost:3001`, Web on `http://localhost:3000`.
|
||||
|
||||
### 5. Seed Data (Optional)
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
|
||||
pnpm --filter @davinci/api exec prisma db seed
|
||||
```
|
||||
|
||||
### VS Code Debugger
|
||||
|
||||
With `start:dev --debug`, attach VS Code's debugger:
|
||||
|
||||
```jsonc
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Davinci API",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Davinci Platform Monorepo │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │
|
||||
│ │ packages/api │ │ packages/web │ │ shared │ │
|
||||
│ │ NestJS 11 │ │ Next.js 15.5 │ │ types │ │
|
||||
│ │ Fastify 5 │ │ App Router │ │ consts │ │
|
||||
│ │ Prisma 7 │ │ Tailwind v4 │ │ │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────────┘ │
|
||||
│ │ │ │
|
||||
│ /api/v1/* /* (frontend) │
|
||||
│ │ │ │
|
||||
│ ┌──────┴──────────────────┴──────┐ │
|
||||
│ │ PostgreSQL │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key design decisions:**
|
||||
|
||||
- **DAVINCI_MODE** — single codebase operates as either `mothership` (control plane) or `community` (per-tenant) via env flag
|
||||
- **NestJS + Fastify** — high-throughput backend with URI-based API versioning (`/api/v1/...`)
|
||||
- **Prisma 7** — type-safe ORM with `@prisma/adapter-pg` driver
|
||||
- **Next.js 15.5** — App Router with `next-intl` for i18n, Tailwind CSS v4, `shadcn/ui`
|
||||
- **Turborepo** — monorepo build orchestration with caching
|
||||
- **Docker multi-stage** — production image includes nginx reverse proxy on port 80
|
||||
|
||||
### API Versioning
|
||||
|
||||
All business endpoints are served under `/api/v1/...`. Operational endpoints are unversioned:
|
||||
|
||||
| Endpoint | Versioned | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `/health` | No | Liveness probe |
|
||||
| `/api-reference` | No | Scalar API documentation |
|
||||
| `/api/v1/*` | Yes | Business endpoints |
|
||||
|
||||
### API Documentation
|
||||
|
||||
Interactive API documentation powered by [Scalar](https://scalar.com) is available at `/api-reference` in development and test environments. Controlled via `ENABLE_API_DOCS` env var (defaults to enabled in dev/test, disabled in production).
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
The API uses NestJS's built-in DI container. Key global providers:
|
||||
|
||||
| Module | Provider | Description |
|
||||
|--------|----------|-------------|
|
||||
| `ConfigModule` | `ConfigService` | Environment variable access (global) |
|
||||
| `PrismaModule` | `PrismaService` | Database access via `prisma.client` (global) |
|
||||
|
||||
Usage in feature modules:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class MyFeatureService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.client.myModel.findMany();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
davinci-platform/
|
||||
├── packages/
|
||||
│ ├── api/ # NestJS 11 backend (Fastify 5 adapter)
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── main.ts # Bootstrap: NestJS + Fastify + versioning + Scalar
|
||||
│ │ │ ├── app.module.ts # Root module (Config, Prisma, Health)
|
||||
│ │ │ ├── docs/ # API documentation (Scalar + OpenAPI)
|
||||
│ │ │ │ └── api-docs.setup.ts # Multi-version Scalar setup
|
||||
│ │ │ ├── health/ # Health module (liveness probe)
|
||||
│ │ │ └── prisma/ # Database module (global PrismaService)
|
||||
│ │ ├── prisma/
|
||||
│ │ │ ├── schema.prisma # Prisma schema
|
||||
│ │ │ └── seed.ts # Idempotent dev seed script
|
||||
│ │ ├── prisma.config.ts # Prisma 7 config (datasource URL, seed)
|
||||
│ │ └── generated/prisma/ # Generated Prisma client (git-ignored)
|
||||
│ ├── web/ # Next.js 15.5 frontend (App Router)
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── app/ # App Router layout + pages
|
||||
│ │ │ ├── i18n/ # next-intl routing + request config
|
||||
│ │ │ ├── middleware.ts # Locale detection middleware
|
||||
│ │ │ └── lib/utils.ts # cn() utility (clsx + tailwind-merge)
|
||||
│ │ ├── messages/en.json # i18n messages
|
||||
│ │ └── components.json # shadcn/ui config
|
||||
│ └── shared/ # Shared types + constants
|
||||
│ └── src/
|
||||
│ ├── index.ts # Barrel export
|
||||
│ └── constants/index.ts # APP_NAME, etc.
|
||||
├── Dockerfile # Multi-stage production build
|
||||
├── nginx.conf # Reverse proxy: /api/* → :3001, /* → :3000
|
||||
├── entrypoint.sh # Production entrypoint
|
||||
├── docker-compose.yml # Dev environment
|
||||
├── docker-compose.test.yml # Isolated test environment
|
||||
├── Makefile # Developer command interface
|
||||
├── turbo.json # Turborepo task config
|
||||
├── smoke-test.sh # 6-endpoint smoke test
|
||||
└── .env.example # Environment template
|
||||
```
|
||||
|
||||
## Makefile Commands
|
||||
|
||||
Run `make help` for the full list. Key commands:
|
||||
|
||||
### Development
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make dev` | Start Postgres + API + Web in watch mode (local apps) |
|
||||
| `make dev-build` | Build and start full dev stack (Docker) |
|
||||
| `make dev-logs` | Tail API container logs |
|
||||
| `make dev-stop` | Stop containers (data preserved) |
|
||||
| `make dev-reset` | Destroy data + rebuild from scratch |
|
||||
| `make dev-seed` | Seed dev database with test data |
|
||||
|
||||
### Testing
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make test` | Full isolated smoke test (build → run → test → teardown) |
|
||||
| `make test:unit` | Unit tests only |
|
||||
| `make test:e2e` | E2E tests only |
|
||||
| `make test:cov` | Tests with coverage report |
|
||||
| `make smoke` | Smoke tests against running dev stack |
|
||||
|
||||
### Code Quality
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make lint` | Lint all packages |
|
||||
| `make format` | Format all files with Prettier |
|
||||
| `make format:check` | Check formatting without writing |
|
||||
|
||||
### Database
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make db:migrate` | Create a new Prisma migration |
|
||||
| `make db:push` | Push schema changes (quick prototyping) |
|
||||
| `make db:studio` | Open Prisma Studio |
|
||||
|
||||
### Docker
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make docker:build` | Build production Docker image |
|
||||
| `make docker:up` | Run full stack via docker-compose |
|
||||
| `make down` | Stop everything (dev + test) |
|
||||
|
||||
## Port Architecture
|
||||
|
||||
Standard ports are used inside containers (Dockerfile, entrypoint, nginx). Docker Compose maps to non-common host ports to avoid conflicts:
|
||||
|
||||
| Stack | API (host → container) | Web (host → container) | Postgres (host → container) |
|
||||
|-------|----------------------|----------------------|---------------------------|
|
||||
| Dev | 4300 → 3001 | 4000 → 3000 | 55434 → 5432 |
|
||||
| Test | 4301 → 3001 | 4001 → 3000 | 45434 → 5432 |
|
||||
| Local (no Docker) | 3001 (direct) | 3000 (direct) | 55434 → 5432 |
|
||||
| Production (nginx) | :80 (reverse proxy) | :80 (reverse proxy) | — |
|
||||
|
||||
> **Ports are chosen to avoid conflicts:** Common tools (React, Vite, Next.js) use 3000/3001/5173. The Docker dev stack uses 4300/4000/55434.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | *(required)* | PostgreSQL connection string |
|
||||
| `PORT` | `3001` | API server port |
|
||||
| `NODE_ENV` | `development` | `development`, `production`, or `test` |
|
||||
| `DAVINCI_MODE` | `mothership` | Operating mode (`mothership` or `community`) |
|
||||
| `ENABLE_API_DOCS` | auto | Scalar API docs (`true`/`false`; defaults enabled in dev/test) |
|
||||
| `CORS_ORIGIN` | `*` (dev) | Comma-separated allowed origins |
|
||||
| `LOG_LEVEL` | auto | Pino log level (`debug` in dev, `info` in prod) |
|
||||
| `NEXT_PUBLIC_API_URL` | — | Frontend API URL (set in docker-compose) |
|
||||
|
||||
See [`.env.example`](.env.example) for a starter template.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology | Version |
|
||||
|-------|------------|---------|
|
||||
| Runtime | Node.js | 22 LTS |
|
||||
| Package Manager | pnpm | 10.32.0 |
|
||||
| Build Orchestration | Turborepo | 2.x |
|
||||
| Backend Framework | NestJS | 11.x |
|
||||
| HTTP Server | Fastify | 5.x |
|
||||
| ORM | Prisma | 7.x |
|
||||
| Frontend Framework | Next.js | 15.5.x |
|
||||
| CSS | Tailwind CSS | v4 |
|
||||
| UI Components | shadcn/ui | — |
|
||||
| i18n | next-intl | 4.x |
|
||||
| Testing | Vitest | 3.x |
|
||||
| Linting | ESLint (flat config) | 9.x |
|
||||
| Formatting | Prettier | 3.4.x |
|
||||
| API Docs | Scalar + @nestjs/swagger | — |
|
||||
| Database | PostgreSQL | 16 |
|
||||
| Container | Docker (multi-stage) | — |
|
||||
| Reverse Proxy | nginx | — |
|
||||
82
docker-compose.test.yml
Normal file
82
docker-compose.test.yml
Normal file
@@ -0,0 +1,82 @@
|
||||
# Standalone test compose for smoke tests.
|
||||
# Fully self-contained — not an overlay of docker-compose.yml.
|
||||
# Uses a separate project name (-p davinci-platform-test) so test volumes,
|
||||
# containers, and networks are fully isolated from the persistent dev stack.
|
||||
#
|
||||
# Usage (via Makefile):
|
||||
# make test # Builds, runs smoke, tears down
|
||||
# make test-up # Start test stack only
|
||||
# make test-down # Tear down test stack
|
||||
#
|
||||
# Port mapping: test → 4301/4001/45434, dev → 4300/4000/55434
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: davinci-platform-test-db
|
||||
ports:
|
||||
- '45434:5432'
|
||||
environment:
|
||||
POSTGRES_USER: davinci
|
||||
POSTGRES_PASSWORD: davinci
|
||||
POSTGRES_DB: davinci_platform_test
|
||||
volumes:
|
||||
- platform-test-pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U davinci -d davinci_platform_test']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- davinci-platform-test
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: api-builder
|
||||
container_name: davinci-platform-test-api
|
||||
command: ['node', 'packages/api/dist/main.js']
|
||||
ports:
|
||||
- '4301:3001'
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://davinci:davinci@db:5432/davinci_platform_test?schema=public
|
||||
PORT: 3001
|
||||
NODE_ENV: test
|
||||
DAVINCI_MODE: mothership
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3001/health || exit 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
- davinci-platform-test
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: web-builder
|
||||
container_name: davinci-platform-test-web
|
||||
command: ['node', 'packages/web/.next/standalone/packages/web/server.js']
|
||||
ports:
|
||||
- '4001:3000'
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:4301/api
|
||||
HOSTNAME: '0.0.0.0'
|
||||
PORT: '3000'
|
||||
networks:
|
||||
- davinci-platform-test
|
||||
|
||||
volumes:
|
||||
platform-test-pgdata:
|
||||
|
||||
networks:
|
||||
davinci-platform-test:
|
||||
name: davinci-platform-test
|
||||
71
docker-compose.yml
Normal file
71
docker-compose.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: davinci-platform-db
|
||||
ports:
|
||||
- '55434:5432'
|
||||
environment:
|
||||
POSTGRES_USER: davinci
|
||||
POSTGRES_PASSWORD: davinci
|
||||
POSTGRES_DB: davinci_platform_dev
|
||||
volumes:
|
||||
- platform-pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U davinci -d davinci_platform_dev']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- davinci-platform
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: api-builder
|
||||
container_name: davinci-platform-api
|
||||
command: ['node', 'packages/api/dist/main.js']
|
||||
ports:
|
||||
- '4300:3001'
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://davinci:davinci@db:5432/davinci_platform_dev?schema=public
|
||||
PORT: 3001
|
||||
NODE_ENV: development
|
||||
DAVINCI_MODE: mothership
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3001/health || exit 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
volumes:
|
||||
- ./packages/api/src:/app/packages/api/src
|
||||
networks:
|
||||
- davinci-platform
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: web-builder
|
||||
container_name: davinci-platform-web
|
||||
command: ['node', 'packages/web/.next/standalone/packages/web/server.js']
|
||||
ports:
|
||||
- '4000:3000'
|
||||
depends_on:
|
||||
- api
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:4300/api
|
||||
HOSTNAME: '0.0.0.0'
|
||||
PORT: '3000'
|
||||
networks:
|
||||
- davinci-platform
|
||||
|
||||
volumes:
|
||||
platform-pgdata:
|
||||
|
||||
networks:
|
||||
davinci-platform:
|
||||
name: davinci-platform
|
||||
0
docs/.gitkeep
Normal file
0
docs/.gitkeep
Normal file
35
entrypoint.sh
Normal file
35
entrypoint.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Known limitation: Multi-process under a single shell entrypoint means if one
|
||||
# process crashes, the container may remain partially alive. Production-grade
|
||||
# process supervision is a future concern, not a scaffold blocker.
|
||||
|
||||
cleanup() {
|
||||
echo "Shutting down..."
|
||||
kill "$NGINX_PID" "$API_PID" "$WEB_PID" 2>/dev/null || true
|
||||
wait "$NGINX_PID" "$API_PID" "$WEB_PID" 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# Start nginx
|
||||
nginx -g 'daemon off;' &
|
||||
NGINX_PID=$!
|
||||
|
||||
# Run Prisma migrations
|
||||
cd /app/packages/api
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Start NestJS API
|
||||
PORT=3001 node dist/main.js &
|
||||
API_PID=$!
|
||||
|
||||
# Start Next.js standalone
|
||||
cd /app/packages/web/standalone
|
||||
PORT=3000 HOSTNAME=0.0.0.0 node packages/web/server.js &
|
||||
WEB_PID=$!
|
||||
|
||||
# Wait for any process to exit
|
||||
wait -n "$NGINX_PID" "$API_PID" "$WEB_PID"
|
||||
22
eslint.config.mjs
Normal file
22
eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
ignores: ['dist/', '.next/', 'node_modules/', 'coverage/', 'prisma/generated/', '.turbo/', '**/next-env.d.ts'],
|
||||
},
|
||||
{
|
||||
files: ['packages/*/src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
},
|
||||
);
|
||||
30
nginx.conf
Normal file
30
nginx.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# API routes
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Frontend routes
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
51
package.json
Normal file
51
package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "davinci-platform",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"private": true,
|
||||
"description": "Davinci Platform — unified monorepo (mothership + community server)",
|
||||
"author": "Galaxis",
|
||||
"license": "UNLICENSED",
|
||||
"packageManager": "pnpm@10.32.0",
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"test": "turbo run test",
|
||||
"test:unit": "turbo run test:unit",
|
||||
"test:e2e": "turbo run test:e2e",
|
||||
"test:cov": "turbo run test:cov",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"packages/*/src/**/*.{ts,tsx}\"",
|
||||
"format:check": "prettier --check \"packages/*/src/**/*.{ts,tsx}\"",
|
||||
"prisma:generate": "pnpm --filter @davinci/api prisma:generate",
|
||||
"prisma:migrate": "pnpm --filter @davinci/api prisma:migrate",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"prettier": "^3.4.0",
|
||||
"turbo": "^2.4.0",
|
||||
"husky": "^9.1.0",
|
||||
"lint-staged": "^15.4.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@nestjs/core",
|
||||
"@parcel/watcher",
|
||||
"@prisma/client",
|
||||
"@prisma/engines",
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"prisma",
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
packages/api/nest-cli.json
Normal file
10
packages/api/nest-cli.json
Normal 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
51
packages/api/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
13
packages/api/prisma.config.ts
Normal file
13
packages/api/prisma.config.ts
Normal 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'],
|
||||
},
|
||||
});
|
||||
14
packages/api/prisma/schema.prisma
Normal file
14
packages/api/prisma/schema.prisma
Normal 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())
|
||||
}
|
||||
43
packages/api/prisma/seed.ts
Normal file
43
packages/api/prisma/seed.ts
Normal 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();
|
||||
});
|
||||
23
packages/api/src/app.module.ts
Normal file
23
packages/api/src/app.module.ts
Normal 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 {}
|
||||
52
packages/api/src/docs/__tests__/api-docs.setup.spec.ts
Normal file
52
packages/api/src/docs/__tests__/api-docs.setup.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
152
packages/api/src/docs/api-docs.setup.ts
Normal file
152
packages/api/src/docs/api-docs.setup.ts
Normal 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>,
|
||||
});
|
||||
}
|
||||
22
packages/api/src/health/health.controller.spec.ts
Normal file
22
packages/api/src/health/health.controller.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
12
packages/api/src/health/health.controller.ts
Normal file
12
packages/api/src/health/health.controller.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
7
packages/api/src/health/health.module.ts
Normal file
7
packages/api/src/health/health.module.ts
Normal 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
57
packages/api/src/main.ts
Normal 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();
|
||||
33
packages/api/src/prisma/__tests__/prisma.service.spec.ts
Normal file
33
packages/api/src/prisma/__tests__/prisma.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
9
packages/api/src/prisma/prisma.module.ts
Normal file
9
packages/api/src/prisma/prisma.module.ts
Normal 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 {}
|
||||
33
packages/api/src/prisma/prisma.service.ts
Normal file
33
packages/api/src/prisma/prisma.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
4
packages/api/tsconfig.build.json
Normal file
4
packages/api/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "node_modules", "**/*.spec.ts", "**/*.test.ts", "test"]
|
||||
}
|
||||
15
packages/api/tsconfig.json
Normal file
15
packages/api/tsconfig.json
Normal 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"]
|
||||
}
|
||||
18
packages/api/vitest.config.ts
Normal file
18
packages/api/vitest.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
17
packages/api/vitest.e2e.config.ts
Normal file
17
packages/api/vitest.e2e.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
20
packages/shared/package.json
Normal file
20
packages/shared/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@davinci/shared",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"description": "Davinci Platform — shared types, DTOs, and constants",
|
||||
"author": "Galaxis",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:cov": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
9
packages/shared/src/__tests__/index.test.ts
Normal file
9
packages/shared/src/__tests__/index.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { APP_NAME } from '../index.js';
|
||||
|
||||
describe('@davinci/shared', () => {
|
||||
it('exports APP_NAME constant', () => {
|
||||
expect(APP_NAME).toBeDefined();
|
||||
expect(APP_NAME).toBe('Davinci Platform');
|
||||
});
|
||||
});
|
||||
1
packages/shared/src/constants/index.ts
Normal file
1
packages/shared/src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const APP_NAME = 'Davinci Platform';
|
||||
1
packages/shared/src/index.ts
Normal file
1
packages/shared/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { APP_NAME } from './constants/index.js';
|
||||
11
packages/shared/tsconfig.json
Normal file
11
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts"]
|
||||
}
|
||||
8
packages/shared/vitest.config.ts
Normal file
8
packages/shared/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
20
packages/web/components.json
Normal file
20
packages/web/components.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
5
packages/web/messages/en.json
Normal file
5
packages/web/messages/en.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Davinci Platform"
|
||||
}
|
||||
}
|
||||
6
packages/web/next-env.d.ts
vendored
Normal file
6
packages/web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
10
packages/web/next.config.ts
Normal file
10
packages/web/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
40
packages/web/package.json
Normal file
40
packages/web/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@davinci/web",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"description": "Davinci Platform — Next.js frontend (App Router)",
|
||||
"author": "Galaxis",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:cov": "vitest run --coverage",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@davinci/shared": "workspace:*",
|
||||
"clsx": "^2.1.0",
|
||||
"next": "^15.5.10",
|
||||
"next-intl": "^4.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.0",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
7
packages/web/postcss.config.mjs
Normal file
7
packages/web/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
packages/web/src/__tests__/page.test.tsx
Normal file
17
packages/web/src/__tests__/page.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import HomePage from '../app/[locale]/page';
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => {
|
||||
const messages: Record<string, string> = { title: 'Davinci Platform' };
|
||||
return messages[key] ?? key;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('HomePage', () => {
|
||||
it('renders the app title', () => {
|
||||
render(<HomePage />);
|
||||
expect(screen.getByText('Davinci Platform')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
1
packages/web/src/__tests__/setup.ts
Normal file
1
packages/web/src/__tests__/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
27
packages/web/src/app/[locale]/layout.tsx
Normal file
27
packages/web/src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({ children, params }: Props) {
|
||||
const { locale } = await params;
|
||||
|
||||
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
11
packages/web/src/app/[locale]/page.tsx
Normal file
11
packages/web/src/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function HomePage() {
|
||||
const t = useTranslations('app');
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||
<h1 className="text-4xl font-bold">{t('title')}</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
82
packages/web/src/app/globals.css
Normal file
82
packages/web/src/app/globals.css
Normal file
@@ -0,0 +1,82 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.965 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.965 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.965 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
11
packages/web/src/app/layout.tsx
Normal file
11
packages/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Davinci Platform',
|
||||
description: 'Davinci Platform — community and mothership',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
15
packages/web/src/i18n/request.ts
Normal file
15
packages/web/src/i18n/request.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from './routing';
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
6
packages/web/src/i18n/routing.ts
Normal file
6
packages/web/src/i18n/routing.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
});
|
||||
6
packages/web/src/lib/utils.ts
Normal file
6
packages/web/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
8
packages/web/src/middleware.ts
Normal file
8
packages/web/src/middleware.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/(en)/:path*'],
|
||||
};
|
||||
18
packages/web/tsconfig.json
Normal file
18
packages/web/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@davinci/shared": ["../shared/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "next-env.d.ts", ".next/types/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
18
packages/web/vitest.config.ts
Normal file
18
packages/web/vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/__tests__/setup.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
|
||||
},
|
||||
},
|
||||
});
|
||||
7783
pnpm-lock.yaml
generated
Normal file
7783
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
pnpm-workspace.yaml
Normal file
10
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
allowBuilds:
|
||||
'@nestjs/core': false
|
||||
'@parcel/watcher': false
|
||||
'@prisma/engines': false
|
||||
'@swc/core': false
|
||||
esbuild: false
|
||||
prisma: false
|
||||
sharp: false
|
||||
0
scripts/.gitkeep
Normal file
0
scripts/.gitkeep
Normal file
108
smoke-test.sh
Normal file
108
smoke-test.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Smoke tests for davinci-platform.
|
||||
# Validates core endpoints are responding correctly.
|
||||
#
|
||||
# Ports are configurable via environment variables so the smoke tests
|
||||
# can run against either the dev stack (3001/3000) or the isolated test
|
||||
# stack (3101/3100).
|
||||
#
|
||||
# Usage:
|
||||
# bash smoke-test.sh # Against dev stack
|
||||
# API_PORT=4301 WEB_PORT=4001 bash smoke-test.sh # Against test stack
|
||||
# SKIP_WEB=1 bash smoke-test.sh # API only (no web container)
|
||||
# make smoke # Against dev stack (via Makefile)
|
||||
|
||||
API="http://localhost:${API_PORT:-4300}"
|
||||
WEB="http://localhost:${WEB_PORT:-4000}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
RESULTS=""
|
||||
|
||||
smoke() {
|
||||
local label="$1"
|
||||
local expect_code="$2"
|
||||
local actual_code="$3"
|
||||
local body="$4"
|
||||
|
||||
if [ "$actual_code" = "$expect_code" ]; then
|
||||
PASS=$((PASS + 1))
|
||||
RESULTS="$RESULTS\n ✅ $label → $actual_code"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
RESULTS="$RESULTS\n ❌ $label → $actual_code (expected $expect_code)"
|
||||
if [ -n "$body" ]; then
|
||||
RESULTS="$RESULTS\n Body: $(echo "$body" | head -c 200)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " DAVINCI PLATFORM SMOKE TESTS"
|
||||
echo " API: $API"
|
||||
echo " Web: $WEB"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# ─── API Tests ────────────────────────────────────────────────────
|
||||
|
||||
# 1. Health endpoint
|
||||
RESP=$(curl -s -w "\n%{http_code}" "$API/health")
|
||||
BODY=$(echo "$RESP" | sed '$d')
|
||||
CODE=$(echo "$RESP" | tail -1)
|
||||
smoke "GET /health" "200" "$CODE" "$BODY"
|
||||
|
||||
# 2. Health response contains status:ok
|
||||
if echo "$BODY" | grep -q '"status":"ok"'; then
|
||||
smoke "Health body contains status:ok" "200" "200"
|
||||
else
|
||||
smoke "Health body contains status:ok" "200" "FAIL" "$BODY"
|
||||
fi
|
||||
|
||||
# 3. Unknown API route returns 404
|
||||
RESP=$(curl -s -w "\n%{http_code}" "$API/api/nonexistent")
|
||||
CODE=$(echo "$RESP" | tail -1)
|
||||
smoke "GET /api/nonexistent" "404" "$CODE"
|
||||
|
||||
# ─── Web Tests ────────────────────────────────────────────────────
|
||||
|
||||
if [ -n "$SKIP_WEB" ]; then
|
||||
echo " ⏭ Skipping web tests (SKIP_WEB is set)"
|
||||
else
|
||||
|
||||
# 4. Web root redirects to locale
|
||||
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$WEB/")
|
||||
smoke "GET / (redirect to locale)" "307" "$RESP"
|
||||
|
||||
# 5. Web /en returns 200
|
||||
RESP=$(curl -sL -w "\n%{http_code}" "$WEB/en")
|
||||
BODY=$(echo "$RESP" | sed '$d')
|
||||
CODE=$(echo "$RESP" | tail -1)
|
||||
smoke "GET /en" "200" "$CODE"
|
||||
|
||||
# 6. Web page contains expected content
|
||||
if echo "$BODY" | grep -q "Davinci Platform"; then
|
||||
smoke "Page contains 'Davinci Platform'" "200" "200"
|
||||
else
|
||||
smoke "Page contains 'Davinci Platform'" "200" "FAIL" "$BODY"
|
||||
fi
|
||||
|
||||
fi # end SKIP_WEB
|
||||
|
||||
# ─── Results ──────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " RESULTS: $PASS passed, $FAIL failed"
|
||||
echo "========================================="
|
||||
echo -e "$RESULTS"
|
||||
echo ""
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "❌ SMOKE TESTS FAILED"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ ALL SMOKE TESTS PASSED"
|
||||
exit 0
|
||||
fi
|
||||
21
tsconfig.base.json
Normal file
21
tsconfig.base.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"target": "ES2022",
|
||||
"sourceMap": true,
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
29
turbo.json
Normal file
29
turbo.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**"]
|
||||
},
|
||||
"dev": {
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test:cov": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"format:check": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user