@mostajs/setup 1.0.0 → 1.1.1
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 +953 -38
- package/dist/api/detect-modules.route.d.ts +3 -0
- package/dist/api/detect-modules.route.js +10 -0
- package/dist/api/install-modules.route.d.ts +7 -0
- package/dist/api/install-modules.route.js +112 -0
- package/dist/api/install.route.d.ts +2 -10
- package/dist/api/install.route.js +5 -9
- package/dist/api/status.route.d.ts +1 -4
- package/dist/api/status.route.js +2 -6
- package/dist/api/test-db.route.d.ts +1 -7
- package/dist/api/test-db.route.js +6 -10
- package/dist/data/dialects.d.ts +1 -1
- package/dist/data/dialects.js +2 -5
- package/dist/data/module-definitions.d.ts +18 -0
- package/dist/data/module-definitions.js +105 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +15 -21
- package/dist/lib/compose-uri.d.ts +1 -1
- package/dist/lib/compose-uri.js +1 -4
- package/dist/lib/db-test.d.ts +1 -1
- package/dist/lib/db-test.js +6 -42
- package/dist/lib/discover-modules.d.ts +13 -0
- package/dist/lib/discover-modules.js +69 -0
- package/dist/lib/env-writer.d.ts +1 -1
- package/dist/lib/env-writer.js +7 -46
- package/dist/lib/menu.d.ts +2 -0
- package/dist/lib/menu.js +16 -0
- package/dist/lib/setup.d.ts +1 -1
- package/dist/lib/setup.js +13 -46
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +1 -2
- package/package.json +30 -19
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
|
[](https://www.npmjs.com/package/@mostajs/setup)
|
|
6
6
|
[](LICENSE)
|
|
@@ -9,71 +9,986 @@ Part of the [@mosta suite](https://mostajs.dev).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
208
|
+
// src/app/api/setup/status/route.ts
|
|
209
|
+
import { createStatusHandler } from '@mostajs/setup'
|
|
210
|
+
import { needsSetup } from '@/lib/setup'
|
|
39
211
|
|
|
40
|
-
|
|
212
|
+
export const { GET } = createStatusHandler(needsSetup)
|
|
213
|
+
```
|
|
214
|
+
|
|
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
|
-
|
|
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
|
|
627
|
+
|
|
628
|
+
# 2. Ouvrir http://localhost:3000
|
|
629
|
+
# → Redirige automatiquement vers /setup
|
|
43
630
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## 4. API Reference
|
|
647
|
+
|
|
648
|
+
### Fonctions principales
|
|
50
649
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 |
|
|
57
657
|
|
|
58
|
-
|
|
658
|
+
### Factories API routes
|
|
659
|
+
|
|
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 |
|
|
667
|
+
|
|
668
|
+
### Data exports
|
|
59
669
|
|
|
60
670
|
| Export | Description |
|
|
61
671
|
|--------|-------------|
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
### Catalogue des 7 modules connus
|
|
756
|
+
|
|
757
|
+
| Module | Package | Requis | Depend de | Standalone | Description |
|
|
758
|
+
|--------|---------|--------|-----------|------------|-------------|
|
|
759
|
+
| **orm** | `@mostajs/orm` | oui | — | — | ORM multi-dialecte (13 SGBD), pattern Hibernate |
|
|
760
|
+
| **auth** | `@mostajs/auth` | oui | orm | non | NextAuth, sessions, hashage mots de passe |
|
|
761
|
+
| **audit** | `@mostajs/audit` | non | orm | non | Journalisation des actions, tracabilite |
|
|
762
|
+
| **rbac** | `@mostajs/rbac` | non | auth, audit | non | Roles, permissions, matrice RBAC |
|
|
763
|
+
| **settings** | `@mostajs/settings` | non | orm | non | Parametres cle-valeur, formulaire auto, provider React |
|
|
764
|
+
| **face** | `@mostajs/face` | non | **aucune** | **oui** | Detection de visage, descripteurs, matching 1:N |
|
|
765
|
+
| **setup** | `@mostajs/setup` | oui | orm | non | Wizard d'installation, test DB, seed runner |
|
|
766
|
+
|
|
767
|
+
### Module standalone : @mostajs/face
|
|
768
|
+
|
|
769
|
+
`@mostajs/face` est **100% independant** — il n'importe aucun package `@mostajs/*` et peut etre utilise dans n'importe quelle application React >= 18 sans `@mostajs/orm` ni base de donnees.
|
|
770
|
+
|
|
771
|
+
**Dependance unique** : `@vladmandic/face-api` (reconnaissance faciale TensorFlow.js)
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
npm install @mostajs/face
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
```tsx
|
|
778
|
+
import { useCamera, useFaceDetection, compareFaces } from '@mostajs/face'
|
|
779
|
+
|
|
780
|
+
// Hooks React pour camera et detection
|
|
781
|
+
const { videoRef, start, stop } = useCamera()
|
|
782
|
+
const { detect, result } = useFaceDetection()
|
|
783
|
+
|
|
784
|
+
// API bas niveau
|
|
785
|
+
import { loadModels, detectFace, extractDescriptor } from '@mostajs/face'
|
|
786
|
+
import { findMatch, descriptorToArray, arrayToDescriptor } from '@mostajs/face'
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Exports : `loadModels`, `detectFace`, `detectAllFaces`, `extractDescriptor`, `compareFaces`, `findMatch`, `findAllMatches`, `descriptorToArray`, `arrayToDescriptor`, `isValidDescriptor`, `drawDetection`, `useCamera`, `useFaceDetection`.
|
|
790
|
+
|
|
791
|
+
### Graphe de dependances
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
┌──────────┐
|
|
795
|
+
│ orm (R) │ R = requis
|
|
796
|
+
└────┬─────┘
|
|
797
|
+
┌────────┼────────┬──────────┐
|
|
798
|
+
v v v v
|
|
799
|
+
┌──────────┐ ┌──────┐ ┌──────────┐ ┌───────┐
|
|
800
|
+
│ auth (R) │ │audit │ │ settings │ │setup(R)│
|
|
801
|
+
└────┬─────┘ └──┬───┘ └──────────┘ └───────┘
|
|
802
|
+
│ │
|
|
803
|
+
v v
|
|
804
|
+
┌──────────────────┐
|
|
805
|
+
│ rbac │
|
|
806
|
+
└──────────────────┘
|
|
807
|
+
|
|
808
|
+
┌──────────────────────────┐
|
|
809
|
+
│ face (100% standalone) │ ← aucune dependance @mostajs
|
|
810
|
+
└──────────────────────────┘
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Liste statique vs decouverte dynamique
|
|
814
|
+
|
|
815
|
+
Le package maintient une **liste statique** des 7 modules ci-dessus avec metadata riches (required, dependsOn, icon, description).
|
|
816
|
+
|
|
817
|
+
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 📦.
|
|
818
|
+
|
|
819
|
+
### Resolution des dependances
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
import { resolveModuleDependencies } from '@mostajs/setup'
|
|
823
|
+
|
|
824
|
+
resolveModuleDependencies(['rbac'])
|
|
825
|
+
// → ['rbac', 'auth', 'audit', 'orm', 'setup']
|
|
826
|
+
// (rbac depend de auth + audit, auth depend de orm, setup est requis)
|
|
827
|
+
|
|
828
|
+
resolveModuleDependencies(['face'])
|
|
829
|
+
// → ['face', 'orm', 'auth', 'setup']
|
|
830
|
+
// (face n'a pas de dependance @mostajs, mais orm/auth/setup sont requis)
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Installation hybride (local + npm)
|
|
834
|
+
|
|
835
|
+
Le handler `install-modules` utilise une strategie hybride :
|
|
836
|
+
|
|
837
|
+
1. **Skip** : si deja dans `node_modules/@mostajs/xxx` → pas de `npm install` (evite hot-reload)
|
|
838
|
+
2. **Local** : si `packages/mosta-xxx/` existe → `npm install file:./packages/mosta-xxx`
|
|
839
|
+
3. **npm registry** : sinon → `npm install @mostajs/xxx`
|
|
840
|
+
|
|
841
|
+
Cela evite les 404 npm pour les packages non encore publies et les hot-reloads Next.js inutiles.
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## 7. Exemples avances
|
|
846
|
+
|
|
847
|
+
### Utiliser runInstall sans le wizard UI
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
import { runInstall } from '@mostajs/setup'
|
|
851
|
+
import { setupConfig } from './my-setup-config'
|
|
852
|
+
|
|
853
|
+
const result = await runInstall(
|
|
854
|
+
{
|
|
855
|
+
dialect: 'postgres',
|
|
856
|
+
db: { host: 'localhost', port: 5432, name: 'mydb', user: 'pg', password: 'secret' },
|
|
857
|
+
admin: { email: 'admin@example.com', password: 'Admin@123', firstName: 'Admin', lastName: 'User' },
|
|
858
|
+
seed: { demoData: true },
|
|
859
|
+
modules: ['orm', 'auth', 'rbac'],
|
|
860
|
+
},
|
|
861
|
+
setupConfig,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
console.log(result)
|
|
865
|
+
// { ok: true, needsRestart: false, seeded: ['categories', 'permissions', 'roles', 'admin', 'demoData'] }
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Ajouter des variables d'environnement personnalisees
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
const config: MostaSetupConfig = {
|
|
872
|
+
appName: 'MyApp',
|
|
873
|
+
extraEnvVars: {
|
|
874
|
+
SMTP_HOST: 'smtp.example.com',
|
|
875
|
+
STRIPE_KEY: 'sk_test_...',
|
|
876
|
+
},
|
|
877
|
+
}
|
|
878
|
+
// → .env.local contiendra aussi SMTP_HOST et STRIPE_KEY
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### Tester la connexion DB programmatiquement
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
import { testDbConnection } from '@mostajs/setup'
|
|
885
|
+
|
|
886
|
+
const result = await testDbConnection({
|
|
887
|
+
dialect: 'mongodb',
|
|
888
|
+
host: 'localhost',
|
|
889
|
+
port: 27017,
|
|
890
|
+
name: 'testdb',
|
|
891
|
+
user: '',
|
|
892
|
+
password: '',
|
|
893
|
+
})
|
|
894
|
+
// { ok: true } ou { ok: false, error: 'Connection refused' }
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Composer une URI manuellement
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
import { composeDbUri } from '@mostajs/setup'
|
|
901
|
+
|
|
902
|
+
composeDbUri('postgres', { host: 'db.example.com', port: 5432, name: 'prod', user: 'app', password: 's3cret' })
|
|
903
|
+
// → 'postgresql://app:s3cret@db.example.com:5432/prod'
|
|
904
|
+
|
|
905
|
+
composeDbUri('sqlite', { host: '', port: 0, name: 'myapp', user: '', password: '' })
|
|
906
|
+
// → './data/myapp.db'
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Ecrire .env.local directement
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
import { writeEnvLocal } from '@mostajs/setup'
|
|
913
|
+
|
|
914
|
+
const dialectChanged = await writeEnvLocal({
|
|
915
|
+
dialect: 'mongodb',
|
|
916
|
+
uri: 'mongodb://localhost:27017/mydb',
|
|
917
|
+
port: 3000,
|
|
918
|
+
extraVars: { MOSTAJS_MODULES: 'orm,auth,rbac' },
|
|
919
|
+
})
|
|
920
|
+
// dialectChanged = true si le dialecte a change (necessite un redemarrage)
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
## 8. FAQ / Troubleshooting
|
|
926
|
+
|
|
927
|
+
### L'installation tourne en boucle (GET /setup se repete)
|
|
928
|
+
|
|
929
|
+
**Cause** : `npm install` modifie `package.json` / `node_modules`, ce qui declenche un hot-reload Next.js et reinitialise le state React.
|
|
930
|
+
|
|
931
|
+
**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).
|
|
932
|
+
|
|
933
|
+
### E11000 duplicate key error (MongoDB)
|
|
934
|
+
|
|
935
|
+
**Cause** : Un champ `unique: true` sans `required: true` dans le schema. MongoDB indexe les `null` et refuse le doublon.
|
|
936
|
+
|
|
937
|
+
**Solution** : Ajoutez `sparse: true` au champ dans votre `EntitySchema`. Cela fonctionne avec MongoDB (index sparse) et est ignore sans risque par les dialectes SQL.
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
clientNumber: { type: 'string', unique: true, sparse: true }
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### JSON.parse: unexpected character at line 1 column 1
|
|
944
|
+
|
|
945
|
+
**Cause** : La reponse de l'API est du HTML (page 404 Next.js) au lieu de JSON, typiquement pendant un hot-reload du serveur.
|
|
946
|
+
|
|
947
|
+
**Solution** : Utilisez un `safeJson()` helper dans le frontend :
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
async function safeJson(res: Response) {
|
|
951
|
+
try { return JSON.parse(await res.text()) }
|
|
952
|
+
catch { return null }
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### npm search timeout (pas d'internet)
|
|
957
|
+
|
|
958
|
+
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.
|
|
959
|
+
|
|
960
|
+
### Comment ajouter un nouveau module a la liste statique ?
|
|
961
|
+
|
|
962
|
+
Editez `packages/mosta-setup/data/module-definitions.ts` et ajoutez une entree a `MODULES` :
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
{
|
|
966
|
+
key: 'notifications',
|
|
967
|
+
packageName: '@mostajs/notifications',
|
|
968
|
+
localDir: 'mosta-notifications', // si disponible localement
|
|
969
|
+
label: 'Notifications',
|
|
970
|
+
description: 'Push, email, SMS',
|
|
971
|
+
icon: '🔔',
|
|
972
|
+
default: false,
|
|
973
|
+
dependsOn: ['orm'],
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
Puis recompilez : `cd packages/mosta-setup && npx tsc`
|
|
978
|
+
|
|
979
|
+
---
|
|
71
980
|
|
|
72
981
|
## Related Packages
|
|
73
982
|
|
|
74
|
-
|
|
75
|
-
|
|
983
|
+
| Package | Depend de orm | Standalone | Description |
|
|
984
|
+
|---------|:---:|:---:|-------------|
|
|
985
|
+
| [@mostajs/orm](https://www.npmjs.com/package/@mostajs/orm) | — | — | Multi-dialect ORM, 13 SGBD (requis) |
|
|
986
|
+
| [@mostajs/auth](https://www.npmjs.com/package/@mostajs/auth) | oui | non | Authentication NextAuth, sessions |
|
|
987
|
+
| [@mostajs/audit](https://www.npmjs.com/package/@mostajs/audit) | oui | non | Audit logging, tracabilite |
|
|
988
|
+
| [@mostajs/rbac](https://www.npmjs.com/package/@mostajs/rbac) | oui | non | Roles & Permissions RBAC |
|
|
989
|
+
| [@mostajs/settings](https://www.npmjs.com/package/@mostajs/settings) | oui | non | Parametres cle-valeur |
|
|
990
|
+
| [@mostajs/face](https://www.npmjs.com/package/@mostajs/face) | **non** | **oui** | Reconnaissance faciale (independant) |
|
|
76
991
|
|
|
77
992
|
## License
|
|
78
993
|
|
|
79
|
-
MIT —
|
|
994
|
+
MIT — (c) 2025 Dr Hamid MADANI <drmdh@msn.com>
|