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