@mostajs/setup 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mostajs/setup
2
2
 
3
- > Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner.
3
+ > Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner, module discovery.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@mostajs/setup.svg)](https://www.npmjs.com/package/@mostajs/setup)
6
6
  [![license](https://img.shields.io/npm/l/@mostajs/setup.svg)](LICENSE)
@@ -9,71 +9,929 @@ Part of the [@mosta suite](https://mostajs.dev).
9
9
 
10
10
  ---
11
11
 
12
- ## Installation
12
+ ## Table des matieres
13
+
14
+ 1. [Installation](#1-installation)
15
+ 2. [Architecture du package](#2-architecture-du-package)
16
+ 3. [Didacticiel : integrer @mostajs/setup dans une nouvelle app](#3-didacticiel--integrer-mostajssetup-dans-une-nouvelle-app)
17
+ - [Etape 1 — Installation des dependances](#etape-1--installation-des-dependances)
18
+ - [Etape 2 — Configurer le setup (lib/setup.ts)](#etape-2--configurer-le-setup-libsetupts)
19
+ - [Etape 3 — Creer les routes API](#etape-3--creer-les-routes-api)
20
+ - [Etape 4 — Creer la page Setup (frontend)](#etape-4--creer-la-page-setup-frontend)
21
+ - [Etape 5 — Middleware : rediriger vers /setup](#etape-5--middleware--rediriger-vers-setup)
22
+ - [Etape 6 — Tester le flux complet](#etape-6--tester-le-flux-complet)
23
+ 4. [API Reference](#4-api-reference)
24
+ 5. [Les 13 dialectes supportes](#5-les-13-dialectes-supportes)
25
+ 6. [Systeme de modules](#6-systeme-de-modules)
26
+ 7. [Exemples avances](#7-exemples-avances)
27
+ 8. [FAQ / Troubleshooting](#8-faq--troubleshooting)
28
+
29
+ ---
30
+
31
+ ## 1. Installation
13
32
 
14
33
  ```bash
15
34
  npm install @mostajs/setup @mostajs/orm
16
35
  ```
17
36
 
18
- ## Quick Start
37
+ `@mostajs/orm` est un peer dependency requis (il fournit les dialectes DB).
38
+
39
+ ---
40
+
41
+ ## 2. Architecture du package
42
+
43
+ ```
44
+ @mostajs/setup
45
+ ├── data/
46
+ │ ├── dialects.ts # Metadata des 13 SGBD (icone, port, driver)
47
+ │ └── module-definitions.ts # Liste statique des modules @mostajs/*
48
+ ├── lib/
49
+ │ ├── setup.ts # needsSetup() + runInstall()
50
+ │ ├── compose-uri.ts # Compose URI de connexion (mongo, pg, mysql...)
51
+ │ ├── db-test.ts # Test de connexion ephemere
52
+ │ ├── env-writer.ts # Ecriture/maj de .env.local
53
+ │ └── discover-modules.ts # Decouverte npm dynamique
54
+ ├── api/
55
+ │ ├── status.route.ts # Factory GET /api/setup/status
56
+ │ ├── test-db.route.ts # Factory POST /api/setup/test-db
57
+ │ ├── install.route.ts # Factory POST /api/setup/install
58
+ │ ├── detect-modules.route.ts # Factory GET /api/setup/detect-modules
59
+ │ └── install-modules.route.ts # Factory POST /api/setup/install-modules
60
+ ├── types/
61
+ │ └── index.ts # Tous les types TypeScript
62
+ └── index.ts # Barrel exports
63
+ ```
64
+
65
+ **Principe** : le package fournit des **factory functions** qui retournent des handlers `{ GET }` ou `{ POST }`. Votre app Next.js les expose en une ligne par route.
66
+
67
+ ---
68
+
69
+ ## 3. Didacticiel : integrer @mostajs/setup dans une nouvelle app
70
+
71
+ ### Prerequis
72
+
73
+ - Next.js 14+ (App Router)
74
+ - Node.js >= 18
75
+ - `@mostajs/orm` installe
76
+
77
+ ### Etape 1 — Installation des dependances
78
+
79
+ ```bash
80
+ npm install @mostajs/setup @mostajs/orm bcryptjs
81
+ npm install -D @types/bcryptjs
82
+ ```
83
+
84
+ ### Etape 2 — Configurer le setup (`lib/setup.ts`)
19
85
 
20
- ### 1. Create setup config
86
+ Ce fichier est le **pont** entre le package generique et votre application. Il definit :
87
+ - Comment compter les utilisateurs (pour `needsSetup`)
88
+ - Comment seeder les roles/permissions
89
+ - Comment creer l'admin
90
+ - Les seeds optionnels propres a votre app
21
91
 
22
92
  ```typescript
23
- import type { MostaSetupConfig } from '@mostajs/setup'
93
+ // src/lib/setup.ts
94
+ import {
95
+ needsSetup as _needsSetup,
96
+ runInstall as _runInstall,
97
+ } from '@mostajs/setup/lib/setup'
98
+ import type {
99
+ DialectType,
100
+ MostaSetupConfig,
101
+ SeedDefinition,
102
+ InstallConfig,
103
+ } from '@mostajs/setup'
24
104
 
25
- export const setupConfig: MostaSetupConfig = {
26
- appName: 'MyApp',
105
+ export type { DialectType }
106
+
107
+ // ─── needsSetup : verifie si la DB a 0 users ──────────────
108
+ export async function needsSetup(): Promise<boolean> {
109
+ return _needsSetup(async () => {
110
+ // Adaptez cette ligne a votre couche d'acces aux donnees
111
+ const { userRepo } = await import('@/dal/service')
112
+ const repo = await userRepo()
113
+ return repo.count()
114
+ })
115
+ }
116
+
117
+ // ─── Seeds optionnels (propres a votre app) ────────────────
118
+ const optionalSeeds: SeedDefinition[] = [
119
+ {
120
+ key: 'demoData',
121
+ label: 'Donnees de demonstration',
122
+ description: 'Quelques enregistrements pour tester',
123
+ run: async () => {
124
+ // Votre logique de seed
125
+ const { productRepo } = await import('@/dal/service')
126
+ const repo = await productRepo()
127
+ await repo.create({ name: 'Produit A', price: 1000 })
128
+ await repo.create({ name: 'Produit B', price: 2000 })
129
+ },
130
+ },
131
+ ]
132
+
133
+ // ─── Configuration du setup ────────────────────────────────
134
+ const SETUP_CONFIG: MostaSetupConfig = {
135
+ appName: 'Mon Application',
27
136
  defaultPort: 3000,
28
- seedRBAC: async () => { /* seed roles & permissions */ },
29
- createAdmin: async ({ email, hashedPassword, firstName, lastName }) => { /* create admin user */ },
137
+
138
+ // Callback pour seeder les roles et permissions
139
+ seedRBAC: async () => {
140
+ const { roleRepo, permissionRepo } = await import('@/dal/service')
141
+ const pRepo = await permissionRepo()
142
+ const rRepo = await roleRepo()
143
+
144
+ // Creer les permissions
145
+ const readPerm = await pRepo.upsert(
146
+ { name: 'read' },
147
+ { name: 'read', description: 'Lecture' },
148
+ )
149
+ const writePerm = await pRepo.upsert(
150
+ { name: 'write' },
151
+ { name: 'write', description: 'Ecriture' },
152
+ )
153
+
154
+ // Creer les roles avec permissions
155
+ await rRepo.upsert(
156
+ { name: 'admin' },
157
+ { name: 'admin', description: 'Administrateur', permissions: [readPerm.id, writePerm.id] },
158
+ )
159
+ await rRepo.upsert(
160
+ { name: 'viewer' },
161
+ { name: 'viewer', description: 'Lecteur', permissions: [readPerm.id] },
162
+ )
163
+ },
164
+
165
+ // Callback pour creer le premier administrateur
166
+ // Le mot de passe est DEJA hashe par le package (bcrypt, 12 rounds)
167
+ createAdmin: async ({ email, hashedPassword, firstName, lastName }) => {
168
+ const { userRepo, roleRepo } = await import('@/dal/service')
169
+ const uRepo = await userRepo()
170
+ const rRepo = await roleRepo()
171
+
172
+ const adminRole = await rRepo.findOne({ name: 'admin' })
173
+ await uRepo.upsert(
174
+ { email: email.toLowerCase() },
175
+ {
176
+ email: email.toLowerCase(),
177
+ password: hashedPassword,
178
+ firstName,
179
+ lastName,
180
+ roles: adminRole ? [adminRole.id] : [],
181
+ status: 'active',
182
+ },
183
+ )
184
+ },
185
+
186
+ optionalSeeds,
187
+ }
188
+
189
+ // ─── runInstall : expose au route handler ──────────────────
190
+ export async function runInstall(config: InstallConfig) {
191
+ return _runInstall(config, SETUP_CONFIG)
30
192
  }
31
193
  ```
32
194
 
33
- ### 2. API routes
195
+ **Points cles :**
196
+ - `needsSetup()` retourne `true` si 0 users en base → l'app doit s'installer
197
+ - `runInstall()` execute les 6 etapes : ecriture `.env.local`, connexion DB, seed RBAC, creation admin, seeds optionnels
198
+ - Le mot de passe admin est **automatiquement hashe** par le package (bcrypt, 12 rounds). Ne le hachez PAS vous-meme.
199
+ - `MOSTAJS_MODULES` est ecrit dans `.env.local` si `config.modules` est fourni
200
+
201
+ ### Etape 3 — Creer les routes API
202
+
203
+ Creez 5 fichiers sous `src/app/api/setup/` :
204
+
205
+ #### 3.1 — Status (GET)
34
206
 
35
207
  ```typescript
36
- // app/api/setup/status/route.ts
37
- import { createStatusHandler, needsSetup } from '@mostajs/setup'
38
- export const { GET } = createStatusHandler(() => needsSetup(countUsers))
208
+ // src/app/api/setup/status/route.ts
209
+ import { createStatusHandler } from '@mostajs/setup'
210
+ import { needsSetup } from '@/lib/setup'
211
+
212
+ export const { GET } = createStatusHandler(needsSetup)
213
+ ```
39
214
 
40
- // app/api/setup/test-db/route.ts
215
+ Reponse : `{ "needsSetup": true }` ou `{ "needsSetup": false }`
216
+
217
+ #### 3.2 — Test connexion DB (POST)
218
+
219
+ ```typescript
220
+ // src/app/api/setup/test-db/route.ts
41
221
  import { createTestDbHandler } from '@mostajs/setup'
42
- export const { POST } = createTestDbHandler(() => needsSetup(countUsers))
222
+ import { needsSetup } from '@/lib/setup'
223
+
224
+ export const { POST } = createTestDbHandler(needsSetup)
225
+ ```
226
+
227
+ Body attendu :
228
+ ```json
229
+ {
230
+ "dialect": "postgres",
231
+ "host": "localhost",
232
+ "port": 5432,
233
+ "name": "mydb",
234
+ "user": "postgres",
235
+ "password": "secret"
236
+ }
237
+ ```
238
+
239
+ Reponse : `{ "ok": true }` ou `{ "ok": false, "error": "..." }`
240
+
241
+ #### 3.3 — Installation (POST)
242
+
243
+ ```typescript
244
+ // src/app/api/setup/install/route.ts
245
+ import { NextRequest, NextResponse } from 'next/server'
246
+ import { needsSetup, runInstall } from '@/lib/setup'
247
+ import { z } from 'zod'
248
+
249
+ const ALL_DIALECTS = [
250
+ 'mongodb', 'sqlite', 'postgres', 'mysql', 'mariadb',
251
+ 'oracle', 'mssql', 'cockroachdb', 'db2', 'hana',
252
+ 'hsqldb', 'spanner', 'sybase',
253
+ ] as const
254
+
255
+ const installSchema = z.object({
256
+ dialect: z.enum(ALL_DIALECTS),
257
+ db: z.object({
258
+ host: z.string().default('localhost'),
259
+ port: z.number().int().min(0).max(65535).default(0),
260
+ name: z.string().min(1),
261
+ user: z.string().default(''),
262
+ password: z.string().default(''),
263
+ }),
264
+ admin: z.object({
265
+ email: z.string().email(),
266
+ password: z.string().min(6),
267
+ firstName: z.string().min(1),
268
+ lastName: z.string().min(1),
269
+ }),
270
+ seed: z.record(z.boolean()).optional(),
271
+ modules: z.array(z.string()).optional(),
272
+ })
273
+
274
+ export async function POST(req: NextRequest) {
275
+ const setupNeeded = await needsSetup()
276
+ if (!setupNeeded) {
277
+ return NextResponse.json(
278
+ { error: { code: 'FORBIDDEN', message: 'Installation deja effectuee' } },
279
+ { status: 403 },
280
+ )
281
+ }
282
+
283
+ const body = await req.json()
284
+ const parsed = installSchema.safeParse(body)
285
+ if (!parsed.success) {
286
+ return NextResponse.json(
287
+ { error: { code: 'VALIDATION_ERROR', details: parsed.error.flatten() } },
288
+ { status: 400 },
289
+ )
290
+ }
291
+
292
+ const result = await runInstall(parsed.data)
293
+ if (!result.ok) {
294
+ return NextResponse.json(
295
+ { error: { code: 'INSTALL_ERROR', message: result.error } },
296
+ { status: 500 },
297
+ )
298
+ }
299
+
300
+ return NextResponse.json({
301
+ data: { ok: true, needsRestart: result.needsRestart, seeded: result.seeded },
302
+ })
303
+ }
304
+ ```
305
+
306
+ #### 3.4 — Detection des modules (GET)
307
+
308
+ ```typescript
309
+ // src/app/api/setup/detect-modules/route.ts
310
+ import { createDetectModulesHandler } from '@mostajs/setup'
311
+
312
+ export const { GET } = createDetectModulesHandler()
313
+ ```
314
+
315
+ Reponse :
316
+ ```json
317
+ {
318
+ "modules": [
319
+ { "key": "orm", "packageName": "@mostajs/orm", "label": "ORM", "required": true, ... },
320
+ { "key": "auth", "packageName": "@mostajs/auth", ... },
321
+ { "key": "new-plugin", "discovered": true, "icon": "📦", ... }
322
+ ],
323
+ "installed": ["orm", "auth", "setup"]
324
+ }
325
+ ```
326
+
327
+ #### 3.5 — Installation des modules (POST)
328
+
329
+ ```typescript
330
+ // src/app/api/setup/install-modules/route.ts
331
+ import { createInstallModulesHandler } from '@mostajs/setup'
332
+ import { needsSetup } from '@/lib/setup'
333
+
334
+ export const { POST } = createInstallModulesHandler(needsSetup)
335
+ ```
336
+
337
+ Body : `{ "modules": ["orm", "auth", "audit", "rbac"] }`
338
+
339
+ Ce handler :
340
+ 1. Resout les dependances transitives (`rbac` → `auth` + `audit`)
341
+ 2. Installe via `npm install` (local `file:./packages/...` si present, sinon npm registry)
342
+ 3. Ecrit `MOSTAJS_MODULES=orm,auth,audit,rbac` dans `.env.local`
343
+
344
+ ### Etape 4 — Creer la page Setup (frontend)
345
+
346
+ Voici un exemple **minimal** de page setup. Adaptez le style a votre design system.
347
+
348
+ ```tsx
349
+ // src/app/setup/page.tsx
350
+ 'use client'
351
+
352
+ import { useState, useEffect } from 'react'
353
+ import { useRouter } from 'next/navigation'
354
+ import { resolveModuleDependencies } from '@mostajs/setup/data/module-definitions'
355
+ import type { ModuleDefinition } from '@mostajs/setup'
356
+
357
+ type Dialect = 'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb'
358
+ | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana'
359
+ | 'hsqldb' | 'spanner' | 'sybase'
360
+
361
+ export default function SetupPage() {
362
+ const router = useRouter()
363
+
364
+ // --- Etat du wizard ---
365
+ const [step, setStep] = useState(1) // 1=modules, 2=db, 3=admin, 4=install
366
+
367
+ // Modules
368
+ const [availableModules, setAvailableModules] = useState<ModuleDefinition[]>([])
369
+ const [selectedModules, setSelectedModules] = useState<string[]>([])
370
+ const [installedModules, setInstalledModules] = useState<string[]>([])
371
+
372
+ // DB
373
+ const [dialect, setDialect] = useState<Dialect>('mongodb')
374
+ const [dbConfig, setDbConfig] = useState({ host: 'localhost', port: 27017, name: 'mydb', user: '', password: '' })
375
+ const [dbOk, setDbOk] = useState(false)
376
+
377
+ // Admin
378
+ const [admin, setAdmin] = useState({ email: '', password: '', firstName: '', lastName: '' })
379
+
380
+ // Install
381
+ const [installing, setInstalling] = useState(false)
382
+ const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null)
383
+
384
+ // --- Charger les modules depuis l'API au montage ---
385
+ useEffect(() => {
386
+ fetch('/api/setup/detect-modules')
387
+ .then(r => r.json())
388
+ .then((data: { modules: ModuleDefinition[]; installed: string[] }) => {
389
+ setAvailableModules(data.modules || [])
390
+ setInstalledModules(data.installed || [])
391
+ // Pre-cocher les modules required + default + deja installes
392
+ const preChecked = new Set([
393
+ ...(data.modules || []).filter(m => m.required || m.default).map(m => m.key),
394
+ ...(data.installed || []),
395
+ ])
396
+ setSelectedModules(Array.from(preChecked))
397
+ })
398
+ .catch(() => {})
399
+ }, [])
400
+
401
+ // --- Toggle un module (avec resolution des dependances) ---
402
+ function toggleModule(key: string) {
403
+ const mod = availableModules.find(m => m.key === key)
404
+ if (!mod || mod.required) return
405
+
406
+ setSelectedModules(prev => {
407
+ if (prev.includes(key)) {
408
+ // Decochage : retirer aussi les modules qui dependent de celui-ci
409
+ const toRemove = new Set([key])
410
+ let changed = true
411
+ while (changed) {
412
+ changed = false
413
+ for (const m of availableModules) {
414
+ if (toRemove.has(m.key) || m.required) continue
415
+ if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
416
+ toRemove.add(m.key)
417
+ changed = true
418
+ }
419
+ }
420
+ }
421
+ return prev.filter(k => !toRemove.has(k))
422
+ } else {
423
+ // Cochage : ajouter les dependances transitives
424
+ return resolveModuleDependencies([...prev, key], availableModules)
425
+ }
426
+ })
427
+ }
428
+
429
+ // --- Tester la connexion DB ---
430
+ async function testDb() {
431
+ const res = await fetch('/api/setup/test-db', {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ body: JSON.stringify({ dialect, ...dbConfig }),
435
+ })
436
+ const data = await res.json()
437
+ setDbOk(data.ok === true)
438
+ }
439
+
440
+ // --- Lancer l'installation ---
441
+ async function install() {
442
+ setInstalling(true)
443
+ setResult(null)
444
+
445
+ // 1. Installer les modules npm
446
+ const modRes = await fetch('/api/setup/install-modules', {
447
+ method: 'POST',
448
+ headers: { 'Content-Type': 'application/json' },
449
+ body: JSON.stringify({ modules: selectedModules }),
450
+ })
451
+ if (!modRes.ok) {
452
+ setResult({ ok: false, error: 'Erreur installation modules' })
453
+ setInstalling(false)
454
+ return
455
+ }
456
+
457
+ // 2. Configurer la DB + seeder
458
+ const res = await fetch('/api/setup/install', {
459
+ method: 'POST',
460
+ headers: { 'Content-Type': 'application/json' },
461
+ body: JSON.stringify({ dialect, db: dbConfig, admin, modules: selectedModules }),
462
+ })
463
+ const data = await res.json()
464
+ setResult(data.data || { ok: false, error: data.error?.message })
465
+ setInstalling(false)
466
+ }
467
+
468
+ return (
469
+ <div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
470
+ <h1>Setup — Mon Application</h1>
471
+
472
+ {/* ── Etape 1 : Modules ── */}
473
+ {step === 1 && (
474
+ <div>
475
+ <h2>1. Modules</h2>
476
+ {availableModules.map(mod => (
477
+ <label key={mod.key} style={{ display: 'block', margin: '8px 0' }}>
478
+ <input
479
+ type="checkbox"
480
+ checked={selectedModules.includes(mod.key)}
481
+ disabled={mod.required}
482
+ onChange={() => toggleModule(mod.key)}
483
+ />
484
+ {' '}{mod.icon} {mod.label}
485
+ {mod.required && <em> (requis)</em>}
486
+ {mod.discovered && <strong> NOUVEAU</strong>}
487
+ {installedModules.includes(mod.key) && <span> ✓</span>}
488
+ </label>
489
+ ))}
490
+ <button onClick={() => setStep(2)}>Suivant →</button>
491
+ </div>
492
+ )}
493
+
494
+ {/* ── Etape 2 : Base de donnees ── */}
495
+ {step === 2 && (
496
+ <div>
497
+ <h2>2. Base de donnees</h2>
498
+ <select value={dialect} onChange={e => setDialect(e.target.value as Dialect)}>
499
+ <option value="mongodb">MongoDB</option>
500
+ <option value="sqlite">SQLite</option>
501
+ <option value="postgres">PostgreSQL</option>
502
+ <option value="mysql">MySQL</option>
503
+ {/* ... autres dialectes ... */}
504
+ </select>
505
+
506
+ {dialect !== 'sqlite' && (
507
+ <>
508
+ <input placeholder="Host" value={dbConfig.host}
509
+ onChange={e => setDbConfig({ ...dbConfig, host: e.target.value })} />
510
+ <input placeholder="Port" type="number" value={dbConfig.port}
511
+ onChange={e => setDbConfig({ ...dbConfig, port: +e.target.value })} />
512
+ <input placeholder="User" value={dbConfig.user}
513
+ onChange={e => setDbConfig({ ...dbConfig, user: e.target.value })} />
514
+ <input placeholder="Password" type="password" value={dbConfig.password}
515
+ onChange={e => setDbConfig({ ...dbConfig, password: e.target.value })} />
516
+ </>
517
+ )}
518
+ <input placeholder="Nom de la base" value={dbConfig.name}
519
+ onChange={e => setDbConfig({ ...dbConfig, name: e.target.value })} />
520
+
521
+ <button onClick={testDb}>Tester la connexion</button>
522
+ {dbOk && <span style={{ color: 'green' }}> ✓ Connexion OK</span>}
523
+
524
+ <br />
525
+ <button onClick={() => setStep(1)}>← Retour</button>
526
+ <button onClick={() => setStep(3)} disabled={dialect !== 'sqlite' && !dbOk}>
527
+ Suivant →
528
+ </button>
529
+ </div>
530
+ )}
531
+
532
+ {/* ── Etape 3 : Administrateur ── */}
533
+ {step === 3 && (
534
+ <div>
535
+ <h2>3. Administrateur</h2>
536
+ <input placeholder="Prenom" value={admin.firstName}
537
+ onChange={e => setAdmin({ ...admin, firstName: e.target.value })} />
538
+ <input placeholder="Nom" value={admin.lastName}
539
+ onChange={e => setAdmin({ ...admin, lastName: e.target.value })} />
540
+ <input placeholder="Email" type="email" value={admin.email}
541
+ onChange={e => setAdmin({ ...admin, email: e.target.value })} />
542
+ <input placeholder="Mot de passe (min 6)" type="password" value={admin.password}
543
+ onChange={e => setAdmin({ ...admin, password: e.target.value })} />
544
+
545
+ <br />
546
+ <button onClick={() => setStep(2)}>← Retour</button>
547
+ <button onClick={() => setStep(4)}>Suivant →</button>
548
+ </div>
549
+ )}
550
+
551
+ {/* ── Etape 4 : Installation ── */}
552
+ {step === 4 && (
553
+ <div>
554
+ <h2>4. Recapitulatif</h2>
555
+ <p><strong>DB :</strong> {dialect} — {dbConfig.name}</p>
556
+ <p><strong>Admin :</strong> {admin.email}</p>
557
+ <p><strong>Modules :</strong> {selectedModules.join(', ')}</p>
558
+
559
+ <button onClick={install} disabled={installing}>
560
+ {installing ? 'Installation en cours...' : 'Installer'}
561
+ </button>
562
+
563
+ {result?.ok && (
564
+ <div style={{ color: 'green', marginTop: 16 }}>
565
+ ✓ Installation terminee !
566
+ <br />
567
+ <button onClick={() => router.push('/login')}>Aller au login →</button>
568
+ </div>
569
+ )}
570
+ {result && !result.ok && (
571
+ <div style={{ color: 'red', marginTop: 16 }}>
572
+ ✗ Erreur : {result.error}
573
+ </div>
574
+ )}
575
+ </div>
576
+ )}
577
+ </div>
578
+ )
579
+ }
580
+ ```
581
+
582
+ ### Etape 5 — Middleware : rediriger vers /setup
583
+
584
+ ```typescript
585
+ // src/middleware.ts
586
+ import { NextResponse } from 'next/server'
587
+ import type { NextRequest } from 'next/server'
588
+
589
+ export async function middleware(req: NextRequest) {
590
+ const { pathname } = req.nextUrl
591
+
592
+ // Laisser passer les routes publiques
593
+ if (
594
+ pathname.startsWith('/setup') ||
595
+ pathname.startsWith('/api/setup') ||
596
+ pathname.startsWith('/api/auth') ||
597
+ pathname.startsWith('/_next')
598
+ ) {
599
+ return NextResponse.next()
600
+ }
601
+
602
+ // Verifier si l'app a besoin du setup
603
+ try {
604
+ const res = await fetch(new URL('/api/setup/status', req.url))
605
+ const data = await res.json()
606
+ if (data.needsSetup) {
607
+ return NextResponse.redirect(new URL('/setup', req.url))
608
+ }
609
+ } catch {
610
+ // Si la DB n'est pas accessible, rediriger vers setup
611
+ return NextResponse.redirect(new URL('/setup', req.url))
612
+ }
613
+
614
+ return NextResponse.next()
615
+ }
616
+
617
+ export const config = {
618
+ matcher: ['/((?!_next|favicon.ico|icons|manifest.json).*)'],
619
+ }
620
+ ```
621
+
622
+ ### Etape 6 — Tester le flux complet
623
+
624
+ ```bash
625
+ # 1. Demarrer le serveur
626
+ npm run dev
43
627
 
44
- // app/api/setup/install/route.ts
45
- import { createInstallHandler } from '@mostajs/setup'
46
- export const { POST } = createInstallHandler(() => needsSetup(countUsers), setupConfig)
628
+ # 2. Ouvrir http://localhost:3000
629
+ # → Redirige automatiquement vers /setup
630
+
631
+ # 3. Suivre le wizard :
632
+ # - Choisir les modules
633
+ # - Selectionner le dialecte (ex: MongoDB, SQLite, Postgres...)
634
+ # - Tester la connexion
635
+ # - Renseigner l'admin
636
+ # - Lancer l'installation
637
+
638
+ # 4. Verifier les resultats :
639
+ # - .env.local contient DB_DIALECT, SGBD_URI, MOSTAJS_MODULES
640
+ # - La DB contient les roles, permissions et l'admin
641
+ # - /api/setup/status retourne { needsSetup: false }
47
642
  ```
48
643
 
49
- ## Features
644
+ ---
645
+
646
+ ## 4. API Reference
647
+
648
+ ### Fonctions principales
649
+
650
+ | Export | Signature | Description |
651
+ |--------|-----------|-------------|
652
+ | `needsSetup` | `(countFn: () => Promise<number>) => Promise<boolean>` | Retourne `true` si 0 users en base |
653
+ | `runInstall` | `(config: InstallConfig, setup: MostaSetupConfig) => Promise<Result>` | Flux complet : env → DB → seed → admin |
654
+ | `testDbConnection` | `(config: DbTestConfig) => Promise<{ ok, error? }>` | Test ephemere (ne modifie pas la connexion globale) |
655
+ | `composeDbUri` | `(dialect: DialectType, db: DbConfig) => string` | Compose l'URI de connexion |
656
+ | `writeEnvLocal` | `(options: EnvWriterOptions) => Promise<boolean>` | Ecrit `.env.local`, retourne `true` si dialect change |
657
+
658
+ ### Factories API routes
50
659
 
51
- - **13 database dialects** all dialects supported by @mostajs/orm
52
- - **Connection testing** — test DB connection before saving
53
- - **URI composition** auto-build connection URI from form fields
54
- - **.env.local writer** persist DB config to disk
55
- - **Seed runner** RBAC + admin user creation
56
- - **Dialect metadata** names, icons, default ports, categories
660
+ | Export | Route | Methode | Description |
661
+ |--------|-------|---------|-------------|
662
+ | `createStatusHandler(needsSetup)` | `/api/setup/status` | GET | Retourne `{ needsSetup: boolean }` |
663
+ | `createTestDbHandler(needsSetup)` | `/api/setup/test-db` | POST | Teste la connexion DB |
664
+ | `createInstallHandler(needsSetup, config)` | `/api/setup/install` | POST | Lance l'installation complete |
665
+ | `createDetectModulesHandler()` | `/api/setup/detect-modules` | GET | Liste modules (statiques + npm) + installes |
666
+ | `createInstallModulesHandler(needsSetup)` | `/api/setup/install-modules` | POST | Installe les modules npm selectionnes |
57
667
 
58
- ## API Reference
668
+ ### Data exports
59
669
 
60
670
  | Export | Description |
61
671
  |--------|-------------|
62
- | `needsSetup(countFn)` | Check if app needs initial setup |
63
- | `runInstall(config)` | Full installation flow |
64
- | `testDbConnection(config)` | Test DB connection |
65
- | `composeDbUri(config)` | Build connection URI |
66
- | `writeEnvLocal(vars)` | Write/update .env.local |
67
- | `DIALECT_INFO` / `ALL_DIALECTS` | Dialect metadata |
68
- | `createStatusHandler()` | GET /setup/status factory |
69
- | `createTestDbHandler()` | POST /setup/test-db factory |
70
- | `createInstallHandler()` | POST /setup/install factory |
672
+ | `DIALECT_INFO` | `Record<DialectType, DialectInfo>` metadata de chaque SGBD |
673
+ | `ALL_DIALECTS` | `DialectType[]` liste des 13 dialectes |
674
+ | `MODULES` | `ModuleDefinition[]` liste statique des 7 modules connus |
675
+ | `resolveModuleDependencies(selected, modules?)` | Resout les dependances transitives |
676
+ | `discoverNpmModules()` | Decouvre les packages `@mostajs/*` sur npm |
677
+
678
+ ### Types
679
+
680
+ ```typescript
681
+ interface InstallConfig {
682
+ dialect: DialectType
683
+ db: DbConfig
684
+ admin: { email: string; password: string; firstName: string; lastName: string }
685
+ seed?: SeedOptions // { activities: true, demoData: false, ... }
686
+ modules?: string[] // ['orm', 'auth', 'rbac'] → ecrit MOSTAJS_MODULES
687
+ }
688
+
689
+ interface MostaSetupConfig {
690
+ appName: string
691
+ defaultPort?: number
692
+ seedRBAC?: () => Promise<void>
693
+ createAdmin?: (admin: { email: string; hashedPassword: string; firstName: string; lastName: string }) => Promise<void>
694
+ optionalSeeds?: SeedDefinition[]
695
+ extraEnvVars?: Record<string, string>
696
+ }
697
+
698
+ interface SeedDefinition {
699
+ key: string
700
+ label: string
701
+ description: string
702
+ icon?: string
703
+ default?: boolean
704
+ run: (repos: any) => Promise<void>
705
+ }
706
+
707
+ interface ModuleDefinition {
708
+ key: string
709
+ packageName: string // '@mostajs/auth'
710
+ localDir?: string // 'mosta-auth' (sous packages/)
711
+ label: string
712
+ description: string
713
+ icon: string
714
+ required?: boolean
715
+ default?: boolean
716
+ dependsOn?: string[]
717
+ discovered?: boolean // true si trouve via npm search
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ## 5. Les 13 dialectes supportes
724
+
725
+ | Dialecte | Icone | Port | Categorie | Driver |
726
+ |----------|-------|------|-----------|--------|
727
+ | MongoDB | 🍃 | 27017 | document | mongoose |
728
+ | SQLite | 📁 | — | file | better-sqlite3 |
729
+ | PostgreSQL | 🐘 | 5432 | sql | pg |
730
+ | MySQL | 🐬 | 3306 | sql | mysql2 |
731
+ | MariaDB | 🦭 | 3306 | sql | mariadb |
732
+ | Oracle | 🔴 | 1521 | enterprise | oracledb |
733
+ | SQL Server | 🟦 | 1433 | enterprise | tedious |
734
+ | CockroachDB | 🪳 | 26257 | distributed | pg |
735
+ | IBM DB2 | 🏢 | 50000 | enterprise | ibm_db |
736
+ | SAP HANA | 💎 | 39013 | enterprise | @sap/hana-client |
737
+ | HyperSQL | ⚡ | 9001 | legacy | — |
738
+ | Cloud Spanner | ☁️ | — | distributed | @google-cloud/spanner |
739
+ | Sybase ASE | 🔷 | 5000 | legacy | sybase |
740
+
741
+ Les URI sont composees automatiquement par `composeDbUri()` :
742
+
743
+ ```
744
+ mongodb://user:pass@host:27017/mydb
745
+ postgresql://user:pass@host:5432/mydb
746
+ mysql://user:pass@host:3306/mydb
747
+ ./data/mydb.db (SQLite)
748
+ spanner://projects/my-project (Cloud Spanner)
749
+ ```
750
+
751
+ ---
752
+
753
+ ## 6. Systeme de modules
754
+
755
+ ### Liste statique vs decouverte dynamique
756
+
757
+ Le package maintient une **liste statique** de 7 modules connus avec metadata riches :
758
+
759
+ ```
760
+ orm (requis) → auth (requis) → rbac → settings
761
+ → audit
762
+ face (optionnel, sans dependance)
763
+ setup (requis)
764
+ ```
765
+
766
+ Au runtime, `GET /api/setup/detect-modules` interroge aussi **npm** (`npm search @mostajs --json`) pour trouver des packages publies apres le deploiement. Ces modules decouverts sont ajoutes avec `discovered: true` et l'icone 📦.
767
+
768
+ ### Resolution des dependances
769
+
770
+ ```typescript
771
+ import { resolveModuleDependencies } from '@mostajs/setup'
772
+
773
+ resolveModuleDependencies(['rbac'])
774
+ // → ['rbac', 'auth', 'audit', 'orm', 'setup']
775
+ // (rbac depend de auth + audit, auth depend de orm, setup est requis)
776
+ ```
777
+
778
+ ### Installation hybride (local + npm)
779
+
780
+ Le handler `install-modules` utilise une strategie hybride :
781
+
782
+ 1. **Local** : si `packages/mosta-xxx/` existe → `npm install file:./packages/mosta-xxx`
783
+ 2. **npm registry** : sinon → `npm install @mostajs/xxx`
784
+ 3. **Skip** : si deja dans `node_modules/@mostajs/xxx` → pas de `npm install`
785
+
786
+ Cela evite les 404 npm pour les packages non encore publies et les hot-reloads Next.js inutiles.
787
+
788
+ ---
789
+
790
+ ## 7. Exemples avances
791
+
792
+ ### Utiliser runInstall sans le wizard UI
793
+
794
+ ```typescript
795
+ import { runInstall } from '@mostajs/setup'
796
+ import { setupConfig } from './my-setup-config'
797
+
798
+ const result = await runInstall(
799
+ {
800
+ dialect: 'postgres',
801
+ db: { host: 'localhost', port: 5432, name: 'mydb', user: 'pg', password: 'secret' },
802
+ admin: { email: 'admin@example.com', password: 'Admin@123', firstName: 'Admin', lastName: 'User' },
803
+ seed: { demoData: true },
804
+ modules: ['orm', 'auth', 'rbac'],
805
+ },
806
+ setupConfig,
807
+ )
808
+
809
+ console.log(result)
810
+ // { ok: true, needsRestart: false, seeded: ['categories', 'permissions', 'roles', 'admin', 'demoData'] }
811
+ ```
812
+
813
+ ### Ajouter des variables d'environnement personnalisees
814
+
815
+ ```typescript
816
+ const config: MostaSetupConfig = {
817
+ appName: 'MyApp',
818
+ extraEnvVars: {
819
+ SMTP_HOST: 'smtp.example.com',
820
+ STRIPE_KEY: 'sk_test_...',
821
+ },
822
+ }
823
+ // → .env.local contiendra aussi SMTP_HOST et STRIPE_KEY
824
+ ```
825
+
826
+ ### Tester la connexion DB programmatiquement
827
+
828
+ ```typescript
829
+ import { testDbConnection } from '@mostajs/setup'
830
+
831
+ const result = await testDbConnection({
832
+ dialect: 'mongodb',
833
+ host: 'localhost',
834
+ port: 27017,
835
+ name: 'testdb',
836
+ user: '',
837
+ password: '',
838
+ })
839
+ // { ok: true } ou { ok: false, error: 'Connection refused' }
840
+ ```
841
+
842
+ ### Composer une URI manuellement
843
+
844
+ ```typescript
845
+ import { composeDbUri } from '@mostajs/setup'
846
+
847
+ composeDbUri('postgres', { host: 'db.example.com', port: 5432, name: 'prod', user: 'app', password: 's3cret' })
848
+ // → 'postgresql://app:s3cret@db.example.com:5432/prod'
849
+
850
+ composeDbUri('sqlite', { host: '', port: 0, name: 'myapp', user: '', password: '' })
851
+ // → './data/myapp.db'
852
+ ```
853
+
854
+ ### Ecrire .env.local directement
855
+
856
+ ```typescript
857
+ import { writeEnvLocal } from '@mostajs/setup'
858
+
859
+ const dialectChanged = await writeEnvLocal({
860
+ dialect: 'mongodb',
861
+ uri: 'mongodb://localhost:27017/mydb',
862
+ port: 3000,
863
+ extraVars: { MOSTAJS_MODULES: 'orm,auth,rbac' },
864
+ })
865
+ // dialectChanged = true si le dialecte a change (necessite un redemarrage)
866
+ ```
867
+
868
+ ---
869
+
870
+ ## 8. FAQ / Troubleshooting
871
+
872
+ ### L'installation tourne en boucle (GET /setup se repete)
873
+
874
+ **Cause** : `npm install` modifie `package.json` / `node_modules`, ce qui declenche un hot-reload Next.js et reinitialise le state React.
875
+
876
+ **Solution** : Le handler `install-modules` **skip les packages deja installes** dans `node_modules/`. Si le probleme persiste, ajoutez une persistence du state wizard dans `sessionStorage` (voir l'implementation de SecuAccess Pro).
877
+
878
+ ### E11000 duplicate key error (MongoDB)
879
+
880
+ **Cause** : Un champ `unique: true` sans `required: true` dans le schema. MongoDB indexe les `null` et refuse le doublon.
881
+
882
+ **Solution** : Ajoutez `sparse: true` au champ dans votre `EntitySchema`. Cela fonctionne avec MongoDB (index sparse) et est ignore sans risque par les dialectes SQL.
883
+
884
+ ```typescript
885
+ clientNumber: { type: 'string', unique: true, sparse: true }
886
+ ```
887
+
888
+ ### JSON.parse: unexpected character at line 1 column 1
889
+
890
+ **Cause** : La reponse de l'API est du HTML (page 404 Next.js) au lieu de JSON, typiquement pendant un hot-reload du serveur.
891
+
892
+ **Solution** : Utilisez un `safeJson()` helper dans le frontend :
893
+
894
+ ```typescript
895
+ async function safeJson(res: Response) {
896
+ try { return JSON.parse(await res.text()) }
897
+ catch { return null }
898
+ }
899
+ ```
900
+
901
+ ### npm search timeout (pas d'internet)
902
+
903
+ Le handler `detect-modules` a un timeout de 10 secondes sur `npm search`. En cas d'echec (offline, timeout), il retourne la **liste statique** des 7 modules connus. Aucune action necessaire.
904
+
905
+ ### Comment ajouter un nouveau module a la liste statique ?
906
+
907
+ Editez `packages/mosta-setup/data/module-definitions.ts` et ajoutez une entree a `MODULES` :
908
+
909
+ ```typescript
910
+ {
911
+ key: 'notifications',
912
+ packageName: '@mostajs/notifications',
913
+ localDir: 'mosta-notifications', // si disponible localement
914
+ label: 'Notifications',
915
+ description: 'Push, email, SMS',
916
+ icon: '🔔',
917
+ default: false,
918
+ dependsOn: ['orm'],
919
+ }
920
+ ```
921
+
922
+ Puis recompilez : `cd packages/mosta-setup && npx tsc`
923
+
924
+ ---
71
925
 
72
926
  ## Related Packages
73
927
 
74
928
  - [@mostajs/orm](https://www.npmjs.com/package/@mostajs/orm) — Multi-dialect ORM (required)
75
- - [@mostajs/auth](https://www.npmjs.com/package/@mostajs/auth) — Authentication (for RBAC seeding)
929
+ - [@mostajs/auth](https://www.npmjs.com/package/@mostajs/auth) — Authentication
930
+ - [@mostajs/audit](https://www.npmjs.com/package/@mostajs/audit) — Audit logging
931
+ - [@mostajs/rbac](https://www.npmjs.com/package/@mostajs/rbac) — Roles & Permissions
932
+ - [@mostajs/settings](https://www.npmjs.com/package/@mostajs/settings) — Key-value settings
933
+ - [@mostajs/face](https://www.npmjs.com/package/@mostajs/face) — Facial recognition
76
934
 
77
935
  ## License
78
936
 
79
- MIT — © 2025 Dr Hamid MADANI <drmdh@msn.com>
937
+ MIT — (c) 2025 Dr Hamid MADANI <drmdh@msn.com>