feat(deploy): image web Docker et docker-compose complet (P5)
- Dockerfile multi-étapes (node:22-alpine, pnpm, output standalone), .dockerignore - docker-compose : service web (hôte 4000) + mysql + mongo avec volumes nommés - Env de connexion selon DB_IMPL (MYSQL_*, MONGODB_URI) ; mongo ?authSource=admin - Seed MySQL via 02-seed.sh (--default-character-set=utf8mb4) pour un UTF-8 propre - fix(api): parseTags gère les colonnes JSON déjà parsées par mysql2 - TODO : phase 5 et DoD cochées
This commit is contained in:
1 parent
17ae195b39
commit
f165168608
9 files changed
+111
-19
No files matched your search
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
coverage
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
docker
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
+1
-1
@@ -13,4 +13,4 @@ MYSQL_DATABASE=flashcards
|
||||
|
||||
# MongoDB (conteneur docker dédié flashcards-mongo, port local 27018)
|
||||
MONGODB_DATABASE=flashcards
|
||||
MONGODB_URI=mongodb://flashcards:flashcards@localhost:27018/flashcards
|
||||
MONGODB_URI=mongodb://flashcards:flashcards@localhost:27018/flashcards?authSource=admin
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-alpine AS base
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
@@ -103,13 +103,13 @@ Les composants d'accès aux données doivent être **le plus générique possibl
|
||||
|
||||
Une feature est « faite » si et seulement si :
|
||||
|
||||
- [ ] Les tests unitaires (Vitest) passent, **coverage** généré et au-dessus du seuil défini
|
||||
- [ ] `lint` (ESLint) et `format` (Prettier — check) sans erreur
|
||||
- [ ] TypeScript strict compile sans erreur
|
||||
- [ ] L'implémentation est branchée sur la **couche générique d'accès aux données**
|
||||
- [ ] Les **deux SGBD** (MySQL + MongoDB) fonctionnent simultanément en local
|
||||
- [ ] Le comportement correspond aux critères d'acceptation de la section 2
|
||||
- [ ] Construit et lançable via docker-compose avec volumes persistants
|
||||
- [x] Les tests unitaires (Vitest) passent, **coverage** généré et au-dessus du seuil défini
|
||||
- [x] `lint` (ESLint) et `format` (Prettier — check) sans erreur
|
||||
- [x] TypeScript strict compile sans erreur
|
||||
- [x] L'implémentation est branchée sur la **couche générique d'accès aux données**
|
||||
- [x] Les **deux SGBD** (MySQL + MongoDB) fonctionnent simultanément en local
|
||||
- [x] Le comportement correspond aux critères d'acceptation de la section 2
|
||||
- [x] Construit et lançable via docker-compose avec volumes persistants
|
||||
|
||||
---
|
||||
|
||||
@@ -182,21 +182,27 @@ Traceabilité F-01 à F-10 (tests unitaires) :
|
||||
|
||||
### Phase 5 — PWA, déploiement
|
||||
|
||||
- [ ] Manifest PWA + service worker (installable, hors-ligne) — la stack SPA le permet
|
||||
- [ ] `docker-compose.yml` : `web`, `mysql`, `mongo`
|
||||
- [ ] Volumes nommés pour l'ensemble des données
|
||||
- [ ] Variables/env de connexion aux 2 SGBD selon l'implémentation sélectionnée
|
||||
- [ ] Construction de l'image web et démarrage complet via `docker compose up`
|
||||
- [ ] Test de persistance après redémarrage des conteneurs
|
||||
- [ ] DoD finale pleinement vérifiée
|
||||
- [x] Manifest PWA + service worker (installable, hors-ligne) — la stack SPA le permet
|
||||
- [x] `docker-compose.yml` : `web`, `mysql`, `mongo`
|
||||
- [x] Volumes nommés pour l'ensemble des données
|
||||
- [x] Variables/env de connexion aux 2 SGBD selon l'implémentation sélectionnée (`DB_IMPL`, `MYSQL_*`, `MONGODB_URI`)
|
||||
- [x] Construction de l'image web et démarrage complet via `docker compose up`
|
||||
- [x] Test de persistance après redémarrage des conteneurs (MySQL + Mongo)
|
||||
- [x] DoD finale pleinement vérifiée
|
||||
|
||||
Notes Phase 5 :
|
||||
- Image web multi-étapes (node:22-alpine, pnpm, `output: 'standalone'`), port hôte **4000** (3000/3001 occupés par l'hôte).
|
||||
- Correction UTF-8 du seed MySQL : `02-seed.sh` avec `mysql --default-character-set=utf8mb4` (le `.sql` brut importait en double-encodage) ; lignes seed réparées en base via `CONVERT(BINARY(CONVERT(... USING latin1)) USING utf8mb4)`.
|
||||
- Correction `parseTags()` dans le repository MySQL (`mysql2` auto-parse les colonnes JSON en tableaux).
|
||||
- Mongo : URI avec `?authSource=admin` (l'utilisateur root est créé dans `admin`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Points à trancher / en attente de décision
|
||||
|
||||
- [ ] Couverture : seuil de coverage exact
|
||||
- [x] Couverture : seuil de coverage exact (>= 80 % imposé, effectif ~95 % stmts)
|
||||
- [ ] Selon cas : `Theme` en table dédiée vs dérivation des `theme` distincts
|
||||
- [ ] Choix par défaut de l'implémentation repository pour la prod (MySQL ou Mongo)
|
||||
- [x] Choix par défaut de l'implémentation repository pour la prod (MySQL par défaut via `DB_IMPL`)
|
||||
- [ ] Authentification éventuelle (hors périmètre README, à confirmer si nécessaire)
|
||||
|
||||
22/09: phase 3 terminée (jeu + navigation intégrés), `pnpm check` vert
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
container_name: flashcards-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DB_IMPL: ${DB_IMPL:-mysql}
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: '3306'
|
||||
MYSQL_USER: flashcards
|
||||
MYSQL_PASSWORD: flashcards
|
||||
MYSQL_DATABASE: flashcards
|
||||
MONGODB_URI: mongodb://flashcards:flashcards@mongo:27017/flashcards?authSource=admin
|
||||
MONGODB_DATABASE: flashcards
|
||||
ports:
|
||||
- '4000:3000'
|
||||
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: flashcards-mysql
|
||||
|
||||
Regular → Executable
+7
@@ -1,3 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Import du seed en forçant l'encodage UTF-8 (sinon les caractères accentués
|
||||
# du fichier source sont interprétés en latin1 et stockés en double-encodage).
|
||||
mysql --default-character-set=utf8mb4 "$MYSQL_DATABASE" <<'SQL'
|
||||
INSERT INTO cards (id, question, response, theme, tags, createdAt, updatedAt) VALUES
|
||||
('10000000-0000-4000-8000-000000000001', 'Qu''est-ce qu''une closure en JavaScript ?', 'Une fonction qui conserve l''accès à la portée de sa définition.', 'javascript', JSON_ARRAY('scope', 'fonction'), NOW(), NOW()),
|
||||
('10000000-0000-4000-8000-000000000002', 'Que retourne typeof null ?', 'La chaîne "object", héritage historique du langage.', 'javascript', JSON_ARRAY('types'), NOW(), NOW()),
|
||||
@@ -9,3 +15,4 @@ INSERT INTO cards (id, question, response, theme, tags, createdAt, updatedAt) VA
|
||||
('10000000-0000-4000-8000-000000000008', 'Différence entre merge et rebase ?', 'merge crée un commit de fusion, rebase réécrit l''historique de façon linéaire.', 'git', JSON_ARRAY('branches'), NOW(), NOW()),
|
||||
('10000000-0000-4000-8000-000000000009', 'Qu''est-ce qu''un volume Docker ?', 'Un stockage persistant indépendant du cycle de vie du conteneur.', 'docker', JSON_ARRAY('stockage'), NOW(), NOW()),
|
||||
('10000000-0000-4000-8000-000000000010', 'Différence entre RUN et CMD dans un Dockerfile ?', 'RUN s''exécute au build, CMD définit la commande lancée au démarrage du conteneur.', 'docker', JSON_ARRAY('dockerfile'), NOW(), NOW());
|
||||
SQL
|
||||
@@ -2,4 +2,5 @@ import { withSerwist } from '@serwist/turbopack'
|
||||
|
||||
export default withSerwist({
|
||||
// Configuration Next.js (Turbopack activé par défaut dans Next 16)
|
||||
output: 'standalone',
|
||||
})
|
||||
@@ -31,7 +31,8 @@ export class MongoCardRepository implements CardRepository {
|
||||
this.collection = collection
|
||||
} else {
|
||||
const client = new MongoClient(
|
||||
process.env.MONGODB_URI ?? 'mongodb://flashcards:flashcards@localhost:27018/flashcards',
|
||||
process.env.MONGODB_URI ??
|
||||
'mongodb://flashcards:flashcards@localhost:27018/flashcards?authSource=admin',
|
||||
)
|
||||
this.collection = client
|
||||
.db(process.env.MONGODB_DATABASE ?? 'flashcards')
|
||||
|
||||
@@ -29,13 +29,28 @@ export class MysqlCardRepository implements CardRepository {
|
||||
})
|
||||
}
|
||||
|
||||
private parseTags(value: string | unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value as string[]
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
private mapRow(row: CardRow): Card {
|
||||
return {
|
||||
id: row.id,
|
||||
question: row.question,
|
||||
response: row.response,
|
||||
theme: row.theme,
|
||||
tags: row.tags ? JSON.parse(row.tags) : [],
|
||||
tags: this.parseTags(row.tags),
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user