@mostajs/setup 1.4.15 → 1.5.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 +205 -2
- package/cli/init.ts +153 -0
- package/dist/api/upload-setup-json.route.d.ts +12 -0
- package/dist/api/upload-setup-json.route.js +64 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +138 -0
- package/dist/components/SetupWizard.js +18 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/compose-uri.js +1 -1
- package/dist/lib/load-setup-json.d.ts +72 -0
- package/dist/lib/load-setup-json.js +163 -0
- package/dist/lib/setup.js +2 -1
- package/package.json +8 -2
- package/schemas/setup.schema.json +218 -0
package/README.md
CHANGED
|
@@ -25,7 +25,8 @@ Part of the [@mosta suite](https://mostajs.dev).
|
|
|
25
25
|
6. [Systeme de modules](#6-systeme-de-modules)
|
|
26
26
|
7. [Exemples avances](#7-exemples-avances)
|
|
27
27
|
8. [Reconfiguration (post-installation)](#8-reconfiguration-post-installation)
|
|
28
|
-
9. [
|
|
28
|
+
9. [Mode declaratif : setup.json](#9-mode-declaratif--setupjson)
|
|
29
|
+
10. [FAQ / Troubleshooting](#10-faq--troubleshooting)
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -497,6 +498,13 @@ npm run dev
|
|
|
497
498
|
| `createDetectModulesHandler()` | `/api/setup/detect-modules` | GET | Liste modules (statiques + npm) + installes |
|
|
498
499
|
| `createInstallModulesHandler(needsSetup)` | `/api/setup/install-modules` | POST | Installe les modules npm selectionnes |
|
|
499
500
|
|
|
501
|
+
### setup.json (declaratif)
|
|
502
|
+
|
|
503
|
+
| Export | Signature | Description |
|
|
504
|
+
|--------|-----------|-------------|
|
|
505
|
+
| `loadSetupJson` | `(source?, repoFactory?) => Promise<MostaSetupConfig>` | Charge `setup.json` et retourne un config complet |
|
|
506
|
+
| `createSetupJsonHandler` | `(needsSetup) => { GET, POST }` | Route API pour verifier/uploader `setup.json` |
|
|
507
|
+
|
|
500
508
|
### Data exports
|
|
501
509
|
|
|
502
510
|
| Export | Description |
|
|
@@ -853,7 +861,202 @@ Un package npm ne peut pas injecter de pages dans le routeur — c'est donc au p
|
|
|
853
861
|
|
|
854
862
|
---
|
|
855
863
|
|
|
856
|
-
## 9.
|
|
864
|
+
## 9. Mode declaratif : setup.json
|
|
865
|
+
|
|
866
|
+
> **Nouveau** — Depuis v1.5, le setup peut etre entierement configure via un fichier JSON declaratif.
|
|
867
|
+
|
|
868
|
+
### Principe
|
|
869
|
+
|
|
870
|
+
Au lieu d'ecrire du TypeScript pour definir les categories, permissions, roles et seeds,
|
|
871
|
+
vous declarez tout dans un fichier `setup.json` a la racine du projet. Le module le lit
|
|
872
|
+
et genere automatiquement les callbacks `seedRBAC`, `createAdmin` et `optionalSeeds`.
|
|
873
|
+
|
|
874
|
+
### Creer un setup.json
|
|
875
|
+
|
|
876
|
+
**3 methodes :**
|
|
877
|
+
|
|
878
|
+
| Methode | Commande | Pour qui |
|
|
879
|
+
|---------|----------|----------|
|
|
880
|
+
| **Studio visuel** | Ouvrir [MostaSetup Studio](https://github.com/apolocine/mosta-setup-studio) | Non-developpeurs, design RBAC |
|
|
881
|
+
| **CLI interactif** | `npx mosta-setup` | Developpeurs en terminal |
|
|
882
|
+
| **CLI rapide** | `npx mosta-setup --quick --name MonApp --port 3000` | CI/CD, scripts |
|
|
883
|
+
|
|
884
|
+
### Structure du fichier
|
|
885
|
+
|
|
886
|
+
```json
|
|
887
|
+
{
|
|
888
|
+
"$schema": "https://mostajs.dev/schemas/setup.v1.json",
|
|
889
|
+
"app": {
|
|
890
|
+
"name": "MonApp",
|
|
891
|
+
"port": 3000,
|
|
892
|
+
"dbNamePrefix": "monappdb"
|
|
893
|
+
},
|
|
894
|
+
"env": {
|
|
895
|
+
"MOSTAJS_MODULES": "orm,auth,audit,rbac,settings,setup"
|
|
896
|
+
},
|
|
897
|
+
"rbac": {
|
|
898
|
+
"categories": [
|
|
899
|
+
{ "name": "admin", "label": "Administration", "icon": "Settings", "order": 0 }
|
|
900
|
+
],
|
|
901
|
+
"permissions": [
|
|
902
|
+
{ "code": "admin:access", "description": "Acceder au panneau", "category": "admin" }
|
|
903
|
+
],
|
|
904
|
+
"roles": [
|
|
905
|
+
{ "name": "admin", "description": "Administrateur", "permissions": ["*"] }
|
|
906
|
+
]
|
|
907
|
+
},
|
|
908
|
+
"seeds": [
|
|
909
|
+
{
|
|
910
|
+
"key": "products",
|
|
911
|
+
"label": "Produits demo",
|
|
912
|
+
"collection": "product",
|
|
913
|
+
"match": "slug",
|
|
914
|
+
"default": true,
|
|
915
|
+
"data": [
|
|
916
|
+
{ "name": "Produit A", "slug": "produit-a", "price": 1000 }
|
|
917
|
+
]
|
|
918
|
+
}
|
|
919
|
+
]
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
Le champ `$schema` active l'**autocompletion dans VS Code** (types, descriptions, exemples).
|
|
924
|
+
|
|
925
|
+
### Utiliser loadSetupJson()
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
// src/lib/setup-config.ts
|
|
929
|
+
import { loadSetupJson } from '@mostajs/setup'
|
|
930
|
+
import type { MostaSetupConfig } from '@mostajs/setup'
|
|
931
|
+
|
|
932
|
+
// repoFactory : adapte a votre couche d'acces aux donnees
|
|
933
|
+
async function repoFactory(collection: string) {
|
|
934
|
+
const service = await import('@/dal/service')
|
|
935
|
+
const factories: Record<string, () => Promise<unknown>> = {
|
|
936
|
+
permissionCategory: service.permissionCategoryRepo,
|
|
937
|
+
permission: service.permissionRepo,
|
|
938
|
+
role: service.roleRepo,
|
|
939
|
+
user: service.userRepo,
|
|
940
|
+
activity: service.activityRepo,
|
|
941
|
+
}
|
|
942
|
+
return factories[collection]() as Promise<any>
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export async function getSetupConfig(): Promise<MostaSetupConfig> {
|
|
946
|
+
return loadSetupJson('./setup.json', repoFactory)
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
```typescript
|
|
951
|
+
// src/app/api/setup/install/route.ts
|
|
952
|
+
import { runInstall } from '@mostajs/setup'
|
|
953
|
+
import type { InstallConfig } from '@mostajs/setup'
|
|
954
|
+
import { appNeedsSetup, getSetupConfig } from '@/lib/setup-config'
|
|
955
|
+
|
|
956
|
+
export async function POST(req: Request) {
|
|
957
|
+
if (!(await appNeedsSetup())) {
|
|
958
|
+
return Response.json({ error: 'Already installed' }, { status: 400 })
|
|
959
|
+
}
|
|
960
|
+
const body: InstallConfig = await req.json()
|
|
961
|
+
const config = await getSetupConfig()
|
|
962
|
+
return Response.json(await runInstall(body, config))
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Fonctionnalites des seeds JSON
|
|
967
|
+
|
|
968
|
+
| Champ | Type | Description |
|
|
969
|
+
|-------|------|-------------|
|
|
970
|
+
| `key` | `string` | Identifiant unique du seed |
|
|
971
|
+
| `label` | `string` | Label affiche dans le wizard (checkbox) |
|
|
972
|
+
| `collection` | `string` | Collection/table cible (doit matcher un schema enregistre) |
|
|
973
|
+
| `match` | `string` | Champ pour upsert idempotent (ex: `slug`, `email`) |
|
|
974
|
+
| `hashField` | `string` | Champ a hasher avec bcrypt avant insertion (ex: `password`) |
|
|
975
|
+
| `roleField` | `string` | Champ contenant un nom de role — resolu en ID a l'execution |
|
|
976
|
+
| `defaults` | `object` | Valeurs par defaut fusionnees dans chaque ligne |
|
|
977
|
+
| `default` | `boolean` | Si `true`, la checkbox est cochee par defaut dans le wizard |
|
|
978
|
+
| `data` | `array` | Tableau d'objets a seeder |
|
|
979
|
+
|
|
980
|
+
**Exemple : seed utilisateurs avec hash + resolution de role :**
|
|
981
|
+
```json
|
|
982
|
+
{
|
|
983
|
+
"key": "demoUsers",
|
|
984
|
+
"collection": "user",
|
|
985
|
+
"match": "email",
|
|
986
|
+
"hashField": "password",
|
|
987
|
+
"roleField": "role",
|
|
988
|
+
"defaults": { "status": "active" },
|
|
989
|
+
"data": [
|
|
990
|
+
{ "email": "agent@app.dz", "password": "Agent@123", "firstName": "Karim", "role": "agent_accueil" }
|
|
991
|
+
]
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
A l'execution :
|
|
996
|
+
1. `password` est hashe avec bcrypt (12 rounds)
|
|
997
|
+
2. `role: "agent_accueil"` est resolu en `roles: ["<id-du-role>"]`
|
|
998
|
+
3. `defaults.status` est fusionne → `status: "active"`
|
|
999
|
+
4. Si `match: "email"` et l'email existe deja → upsert (pas de doublon)
|
|
1000
|
+
|
|
1001
|
+
### setup.json manquant : upload automatique
|
|
1002
|
+
|
|
1003
|
+
Si le projet accede a `/setup` et que `setup.json` n'existe pas, la page affiche
|
|
1004
|
+
automatiquement un formulaire d'upload (drag & drop ou selection de fichier).
|
|
1005
|
+
|
|
1006
|
+
Pour activer cette fonctionnalite, ajoutez la route API :
|
|
1007
|
+
|
|
1008
|
+
```typescript
|
|
1009
|
+
// src/app/api/setup/setup-json/route.ts
|
|
1010
|
+
import { createSetupJsonHandler } from '@mostajs/setup'
|
|
1011
|
+
import { appNeedsSetup } from '@/lib/setup-config'
|
|
1012
|
+
|
|
1013
|
+
export const { GET, POST } = createSetupJsonHandler(appNeedsSetup)
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
- **GET** `/api/setup/setup-json` → `{ exists: boolean, config?: {...} }`
|
|
1017
|
+
- **POST** `/api/setup/setup-json` → recoit le JSON, ecrit `./setup.json`
|
|
1018
|
+
|
|
1019
|
+
### Mixer JSON + code TypeScript
|
|
1020
|
+
|
|
1021
|
+
Les seeds simples (insert de donnees) vont dans `setup.json`. Les seeds complexes
|
|
1022
|
+
(relations, logique conditionnelle) restent en TypeScript :
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
const config = await loadSetupJson('./setup.json', repoFactory)
|
|
1026
|
+
// Ajouter un seed code-only
|
|
1027
|
+
config.optionalSeeds = [
|
|
1028
|
+
...(config.optionalSeeds ?? []),
|
|
1029
|
+
{ key: 'demoData', label: 'Donnees complexes', run: async () => { /* ... */ } },
|
|
1030
|
+
]
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### Validation
|
|
1034
|
+
|
|
1035
|
+
`loadSetupJson()` valide automatiquement :
|
|
1036
|
+
- `app.name` est requis
|
|
1037
|
+
- Chaque permission reference une categorie existante
|
|
1038
|
+
- Chaque role reference des permissions existantes (sauf `*`)
|
|
1039
|
+
- Erreur descriptive en cas de reference croisee invalide
|
|
1040
|
+
|
|
1041
|
+
### CLI : npx mosta-setup
|
|
1042
|
+
|
|
1043
|
+
```bash
|
|
1044
|
+
# Mode interactif (terminal)
|
|
1045
|
+
npx mosta-setup
|
|
1046
|
+
|
|
1047
|
+
# Mode rapide (CI, scripts, Dockerfile)
|
|
1048
|
+
npx mosta-setup --quick --name MonApp --port 4567 --db monappdb
|
|
1049
|
+
|
|
1050
|
+
# Avec modules
|
|
1051
|
+
npx mosta-setup --quick --name MonApp --modules "orm,auth,audit,rbac,settings,setup"
|
|
1052
|
+
|
|
1053
|
+
# Sortie stdout (pour pipe)
|
|
1054
|
+
npx mosta-setup --quick --name MonApp --stdout | jq .
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
## 10. FAQ / Troubleshooting
|
|
857
1060
|
|
|
858
1061
|
### L'installation tourne en boucle (GET /setup se repete)
|
|
859
1062
|
|
package/cli/init.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
// CLI: npx @mostajs/setup init — interactive setup.json generator
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// npx mosta-setup # interactive mode
|
|
7
|
+
// npx mosta-setup --quick # generate minimal setup.json with defaults
|
|
8
|
+
// npx mosta-setup --name MyApp --port 4567 --db myappdb # non-interactive
|
|
9
|
+
|
|
10
|
+
import * as readline from 'readline'
|
|
11
|
+
import * as fs from 'fs'
|
|
12
|
+
import * as path from 'path'
|
|
13
|
+
|
|
14
|
+
const CYAN = '\x1b[36m'
|
|
15
|
+
const GREEN = '\x1b[32m'
|
|
16
|
+
const NC = '\x1b[0m'
|
|
17
|
+
|
|
18
|
+
// ── Parse CLI args ───────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2)
|
|
21
|
+
const getArg = (flag: string): string | undefined => {
|
|
22
|
+
const i = args.indexOf(flag)
|
|
23
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined
|
|
24
|
+
}
|
|
25
|
+
const hasFlag = (flag: string) => args.includes(flag)
|
|
26
|
+
|
|
27
|
+
// ── Quick mode (non-interactive) ─────────────────────────
|
|
28
|
+
|
|
29
|
+
if (hasFlag('--quick') || hasFlag('-q')) {
|
|
30
|
+
const name = getArg('--name') || 'MonApp'
|
|
31
|
+
const port = parseInt(getArg('--port') || '3000')
|
|
32
|
+
const dbPrefix = getArg('--db') || name.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db'
|
|
33
|
+
const modules = getArg('--modules')
|
|
34
|
+
|
|
35
|
+
const setup: Record<string, unknown> = {
|
|
36
|
+
$schema: 'https://mostajs.dev/schemas/setup.v1.json',
|
|
37
|
+
app: { name, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
|
|
38
|
+
}
|
|
39
|
+
if (modules) setup.env = { MOSTAJS_MODULES: modules }
|
|
40
|
+
|
|
41
|
+
const json = JSON.stringify(setup, null, 2)
|
|
42
|
+
const outPath = path.resolve(process.cwd(), 'setup.json')
|
|
43
|
+
|
|
44
|
+
if (hasFlag('--stdout')) {
|
|
45
|
+
process.stdout.write(json + '\n')
|
|
46
|
+
} else {
|
|
47
|
+
fs.writeFileSync(outPath, json + '\n', 'utf-8')
|
|
48
|
+
console.log(`${GREEN}setup.json cree: ${outPath}${NC}`)
|
|
49
|
+
}
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Interactive mode ─────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY !== false })
|
|
56
|
+
const ask = (q: string, def?: string): Promise<string> =>
|
|
57
|
+
new Promise(r => rl.question(`${q}${def ? ` (${def})` : ''}: `, a => r(a.trim() || def || '')))
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
console.log(`\n${CYAN} MostaSetup — Generateur de setup.json${NC}\n`)
|
|
61
|
+
|
|
62
|
+
// ── App ────────────────────────────────────────────────
|
|
63
|
+
const appName = await ask(' Nom de l\'application', 'MonApp')
|
|
64
|
+
const portStr = await ask(' Port HTTP', '3000')
|
|
65
|
+
const port = parseInt(portStr) || 3000
|
|
66
|
+
const dbPrefix = await ask(' Prefix DB', appName.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db')
|
|
67
|
+
|
|
68
|
+
// ── Env ────────────────────────────────────────────────
|
|
69
|
+
const addModules = (await ask(' Ajouter MOSTAJS_MODULES ? (o/n)', 'o')).toLowerCase() === 'o'
|
|
70
|
+
const modules = addModules
|
|
71
|
+
? await ask(' Modules', 'orm,auth,audit,rbac,settings,setup')
|
|
72
|
+
: ''
|
|
73
|
+
|
|
74
|
+
// ── RBAC ───────────────────────────────────────────────
|
|
75
|
+
const addRbac = (await ask('\n Generer un RBAC de base ? (o/n)', 'o')).toLowerCase() === 'o'
|
|
76
|
+
const categories: { name: string; label: string }[] = []
|
|
77
|
+
const permissions: { code: string; description: string; category: string }[] = []
|
|
78
|
+
const roles: { name: string; description: string; permissions: string[] }[] = []
|
|
79
|
+
|
|
80
|
+
if (addRbac) {
|
|
81
|
+
console.log(`\n ${CYAN}Categories${NC} (entree vide pour terminer)`)
|
|
82
|
+
while (true) {
|
|
83
|
+
const name = await ask(' Nom categorie (ex: admin)')
|
|
84
|
+
if (!name) break
|
|
85
|
+
const label = await ask(` Label pour "${name}"`, name.charAt(0).toUpperCase() + name.slice(1))
|
|
86
|
+
categories.push({ name, label })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`\n ${CYAN}Permissions${NC} (entree vide pour terminer)`)
|
|
90
|
+
while (true) {
|
|
91
|
+
const code = await ask(' Code permission (ex: client:view)')
|
|
92
|
+
if (!code) break
|
|
93
|
+
const desc = await ask(` Description pour "${code}"`, code)
|
|
94
|
+
const cat = categories.length > 0
|
|
95
|
+
? await ask(` Categorie (${categories.map(c => c.name).join(', ')})`, categories[0].name)
|
|
96
|
+
: ''
|
|
97
|
+
permissions.push({ code, description: desc, category: cat })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`\n ${CYAN}Roles${NC} (entree vide pour terminer)`)
|
|
101
|
+
while (true) {
|
|
102
|
+
const name = await ask(' Nom role (ex: admin)')
|
|
103
|
+
if (!name) break
|
|
104
|
+
const desc = await ask(` Description`, name)
|
|
105
|
+
const allPerms = (await ask(' Toutes les permissions ? (o/n)', name === 'admin' ? 'o' : 'n')).toLowerCase() === 'o'
|
|
106
|
+
let rolePerms: string[] = []
|
|
107
|
+
if (allPerms) {
|
|
108
|
+
rolePerms = ['*']
|
|
109
|
+
} else {
|
|
110
|
+
const permsStr = await ask(` Permissions (virgules: ${permissions.map(p => p.code).slice(0, 5).join(', ')}...)`)
|
|
111
|
+
rolePerms = permsStr.split(',').map(s => s.trim()).filter(Boolean)
|
|
112
|
+
}
|
|
113
|
+
roles.push({ name, description: desc, permissions: rolePerms })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Build JSON ─────────────────────────────────────────
|
|
118
|
+
const setup: Record<string, unknown> = {
|
|
119
|
+
$schema: 'https://mostajs.dev/schemas/setup.v1.json',
|
|
120
|
+
app: { name: appName, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
|
|
121
|
+
}
|
|
122
|
+
if (modules) {
|
|
123
|
+
setup.env = { MOSTAJS_MODULES: modules }
|
|
124
|
+
}
|
|
125
|
+
if (addRbac && (categories.length || permissions.length || roles.length)) {
|
|
126
|
+
const rbac: Record<string, unknown> = {}
|
|
127
|
+
if (categories.length) rbac.categories = categories
|
|
128
|
+
if (permissions.length) rbac.permissions = permissions
|
|
129
|
+
if (roles.length) rbac.roles = roles
|
|
130
|
+
setup.rbac = rbac
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const json = JSON.stringify(setup, null, 2)
|
|
134
|
+
const outPath = path.resolve(process.cwd(), 'setup.json')
|
|
135
|
+
|
|
136
|
+
console.log(`\n${CYAN} Preview:${NC}`)
|
|
137
|
+
console.log(json)
|
|
138
|
+
|
|
139
|
+
const write = (await ask(`\n Ecrire dans ${outPath} ? (o/n)`, 'o')).toLowerCase() === 'o'
|
|
140
|
+
if (write) {
|
|
141
|
+
fs.writeFileSync(outPath, json + '\n', 'utf-8')
|
|
142
|
+
console.log(`\n ${GREEN}setup.json cree avec succes${NC}\n`)
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`\n Annule.\n`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
rl.close()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch(err => {
|
|
151
|
+
console.error(err)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type NeedsSetupFn = () => Promise<boolean>;
|
|
2
|
+
/**
|
|
3
|
+
* Creates handlers for checking and uploading setup.json.
|
|
4
|
+
*
|
|
5
|
+
* GET /api/setup/setup-json → { exists: boolean, config?: { appName, seeds, rbac } }
|
|
6
|
+
* POST /api/setup/setup-json → receives JSON body, writes to ./setup.json
|
|
7
|
+
*/
|
|
8
|
+
export declare function createSetupJsonHandler(needsSetup: NeedsSetupFn): {
|
|
9
|
+
GET: () => Promise<Response>;
|
|
10
|
+
POST: (req: Request) => Promise<Response>;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @mostajs/setup — API Route for uploading setup.json
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
//
|
|
4
|
+
// When a project uses loadSetupJson() but setup.json is missing,
|
|
5
|
+
// this route allows uploading it via the setup wizard UI.
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
/**
|
|
9
|
+
* Creates handlers for checking and uploading setup.json.
|
|
10
|
+
*
|
|
11
|
+
* GET /api/setup/setup-json → { exists: boolean, config?: { appName, seeds, rbac } }
|
|
12
|
+
* POST /api/setup/setup-json → receives JSON body, writes to ./setup.json
|
|
13
|
+
*/
|
|
14
|
+
export function createSetupJsonHandler(needsSetup) {
|
|
15
|
+
const setupJsonPath = () => path.resolve(process.cwd(), 'setup.json');
|
|
16
|
+
async function GET() {
|
|
17
|
+
const filePath = setupJsonPath();
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
return Response.json({ exists: false });
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
const json = JSON.parse(raw);
|
|
24
|
+
return Response.json({
|
|
25
|
+
exists: true,
|
|
26
|
+
config: {
|
|
27
|
+
appName: json.app?.name,
|
|
28
|
+
hasRbac: !!(json.rbac?.roles?.length || json.rbac?.permissions?.length),
|
|
29
|
+
seedCount: json.seeds?.length ?? 0,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : 'Invalid JSON';
|
|
35
|
+
return Response.json({ exists: false, error: msg });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function POST(req) {
|
|
39
|
+
if (!(await needsSetup())) {
|
|
40
|
+
return Response.json({ error: 'Already installed' }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = await req.json();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
if (!body.app?.name) {
|
|
50
|
+
return Response.json({ error: 'setup.json must have app.name' }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
const filePath = setupJsonPath();
|
|
53
|
+
fs.writeFileSync(filePath, JSON.stringify(body, null, 2) + '\n', 'utf-8');
|
|
54
|
+
return Response.json({
|
|
55
|
+
ok: true,
|
|
56
|
+
config: {
|
|
57
|
+
appName: body.app.name,
|
|
58
|
+
hasRbac: !!(body.rbac?.roles?.length || body.rbac?.permissions?.length),
|
|
59
|
+
seedCount: body.seeds?.length ?? 0,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { GET, POST };
|
|
64
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
// CLI: npx @mostajs/setup init — interactive setup.json generator
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// npx mosta-setup # interactive mode
|
|
7
|
+
// npx mosta-setup --quick # generate minimal setup.json with defaults
|
|
8
|
+
// npx mosta-setup --name MyApp --port 4567 --db myappdb # non-interactive
|
|
9
|
+
import * as readline from 'readline';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
const CYAN = '\x1b[36m';
|
|
13
|
+
const GREEN = '\x1b[32m';
|
|
14
|
+
const NC = '\x1b[0m';
|
|
15
|
+
// ── Parse CLI args ───────────────────────────────────────
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const getArg = (flag) => {
|
|
18
|
+
const i = args.indexOf(flag);
|
|
19
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
20
|
+
};
|
|
21
|
+
const hasFlag = (flag) => args.includes(flag);
|
|
22
|
+
// ── Quick mode (non-interactive) ─────────────────────────
|
|
23
|
+
if (hasFlag('--quick') || hasFlag('-q')) {
|
|
24
|
+
const name = getArg('--name') || 'MonApp';
|
|
25
|
+
const port = parseInt(getArg('--port') || '3000');
|
|
26
|
+
const dbPrefix = getArg('--db') || name.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db';
|
|
27
|
+
const modules = getArg('--modules');
|
|
28
|
+
const setup = {
|
|
29
|
+
$schema: 'https://mostajs.dev/schemas/setup.v1.json',
|
|
30
|
+
app: { name, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
|
|
31
|
+
};
|
|
32
|
+
if (modules)
|
|
33
|
+
setup.env = { MOSTAJS_MODULES: modules };
|
|
34
|
+
const json = JSON.stringify(setup, null, 2);
|
|
35
|
+
const outPath = path.resolve(process.cwd(), 'setup.json');
|
|
36
|
+
if (hasFlag('--stdout')) {
|
|
37
|
+
process.stdout.write(json + '\n');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
fs.writeFileSync(outPath, json + '\n', 'utf-8');
|
|
41
|
+
console.log(`${GREEN}setup.json cree: ${outPath}${NC}`);
|
|
42
|
+
}
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
// ── Interactive mode ─────────────────────────────────────
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY !== false });
|
|
47
|
+
const ask = (q, def) => new Promise(r => rl.question(`${q}${def ? ` (${def})` : ''}: `, a => r(a.trim() || def || '')));
|
|
48
|
+
async function main() {
|
|
49
|
+
console.log(`\n${CYAN} MostaSetup — Generateur de setup.json${NC}\n`);
|
|
50
|
+
// ── App ────────────────────────────────────────────────
|
|
51
|
+
const appName = await ask(' Nom de l\'application', 'MonApp');
|
|
52
|
+
const portStr = await ask(' Port HTTP', '3000');
|
|
53
|
+
const port = parseInt(portStr) || 3000;
|
|
54
|
+
const dbPrefix = await ask(' Prefix DB', appName.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db');
|
|
55
|
+
// ── Env ────────────────────────────────────────────────
|
|
56
|
+
const addModules = (await ask(' Ajouter MOSTAJS_MODULES ? (o/n)', 'o')).toLowerCase() === 'o';
|
|
57
|
+
const modules = addModules
|
|
58
|
+
? await ask(' Modules', 'orm,auth,audit,rbac,settings,setup')
|
|
59
|
+
: '';
|
|
60
|
+
// ── RBAC ───────────────────────────────────────────────
|
|
61
|
+
const addRbac = (await ask('\n Generer un RBAC de base ? (o/n)', 'o')).toLowerCase() === 'o';
|
|
62
|
+
const categories = [];
|
|
63
|
+
const permissions = [];
|
|
64
|
+
const roles = [];
|
|
65
|
+
if (addRbac) {
|
|
66
|
+
console.log(`\n ${CYAN}Categories${NC} (entree vide pour terminer)`);
|
|
67
|
+
while (true) {
|
|
68
|
+
const name = await ask(' Nom categorie (ex: admin)');
|
|
69
|
+
if (!name)
|
|
70
|
+
break;
|
|
71
|
+
const label = await ask(` Label pour "${name}"`, name.charAt(0).toUpperCase() + name.slice(1));
|
|
72
|
+
categories.push({ name, label });
|
|
73
|
+
}
|
|
74
|
+
console.log(`\n ${CYAN}Permissions${NC} (entree vide pour terminer)`);
|
|
75
|
+
while (true) {
|
|
76
|
+
const code = await ask(' Code permission (ex: client:view)');
|
|
77
|
+
if (!code)
|
|
78
|
+
break;
|
|
79
|
+
const desc = await ask(` Description pour "${code}"`, code);
|
|
80
|
+
const cat = categories.length > 0
|
|
81
|
+
? await ask(` Categorie (${categories.map(c => c.name).join(', ')})`, categories[0].name)
|
|
82
|
+
: '';
|
|
83
|
+
permissions.push({ code, description: desc, category: cat });
|
|
84
|
+
}
|
|
85
|
+
console.log(`\n ${CYAN}Roles${NC} (entree vide pour terminer)`);
|
|
86
|
+
while (true) {
|
|
87
|
+
const name = await ask(' Nom role (ex: admin)');
|
|
88
|
+
if (!name)
|
|
89
|
+
break;
|
|
90
|
+
const desc = await ask(` Description`, name);
|
|
91
|
+
const allPerms = (await ask(' Toutes les permissions ? (o/n)', name === 'admin' ? 'o' : 'n')).toLowerCase() === 'o';
|
|
92
|
+
let rolePerms = [];
|
|
93
|
+
if (allPerms) {
|
|
94
|
+
rolePerms = ['*'];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const permsStr = await ask(` Permissions (virgules: ${permissions.map(p => p.code).slice(0, 5).join(', ')}...)`);
|
|
98
|
+
rolePerms = permsStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
roles.push({ name, description: desc, permissions: rolePerms });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Build JSON ─────────────────────────────────────────
|
|
104
|
+
const setup = {
|
|
105
|
+
$schema: 'https://mostajs.dev/schemas/setup.v1.json',
|
|
106
|
+
app: { name: appName, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
|
|
107
|
+
};
|
|
108
|
+
if (modules) {
|
|
109
|
+
setup.env = { MOSTAJS_MODULES: modules };
|
|
110
|
+
}
|
|
111
|
+
if (addRbac && (categories.length || permissions.length || roles.length)) {
|
|
112
|
+
const rbac = {};
|
|
113
|
+
if (categories.length)
|
|
114
|
+
rbac.categories = categories;
|
|
115
|
+
if (permissions.length)
|
|
116
|
+
rbac.permissions = permissions;
|
|
117
|
+
if (roles.length)
|
|
118
|
+
rbac.roles = roles;
|
|
119
|
+
setup.rbac = rbac;
|
|
120
|
+
}
|
|
121
|
+
const json = JSON.stringify(setup, null, 2);
|
|
122
|
+
const outPath = path.resolve(process.cwd(), 'setup.json');
|
|
123
|
+
console.log(`\n${CYAN} Preview:${NC}`);
|
|
124
|
+
console.log(json);
|
|
125
|
+
const write = (await ask(`\n Ecrire dans ${outPath} ? (o/n)`, 'o')).toLowerCase() === 'o';
|
|
126
|
+
if (write) {
|
|
127
|
+
fs.writeFileSync(outPath, json + '\n', 'utf-8');
|
|
128
|
+
console.log(`\n ${GREEN}setup.json cree avec succes${NC}\n`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`\n Annule.\n`);
|
|
132
|
+
}
|
|
133
|
+
rl.close();
|
|
134
|
+
}
|
|
135
|
+
main().catch(err => {
|
|
136
|
+
console.error(err);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
5
5
|
import { useState, useEffect, useCallback } from 'react';
|
|
6
6
|
// ── Constants ────────────────────────────────────────────────
|
|
7
|
-
const
|
|
7
|
+
const ALL_STEPS = ['welcome', 'modules', 'dialect', 'database', 'admin', 'summary'];
|
|
8
8
|
const DIALECT_DEFAULTS = {
|
|
9
9
|
mongodb: { host: 'localhost', port: 27017, name: 'mydb_prod', user: '', password: '' },
|
|
10
10
|
sqlite: { host: '', port: 0, name: 'mydb', user: '', password: '' },
|
|
@@ -346,14 +346,19 @@ function JarUploadInline({ dialect, jarEndpoint, dbConfig }) {
|
|
|
346
346
|
// ── Main Component ───────────────────────────────────────────
|
|
347
347
|
export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNamePrefix = 'mydb', persistState = true, }) {
|
|
348
348
|
const t = tProp || ((k) => k);
|
|
349
|
+
// Modules step is only shown if detectModules endpoint is explicitly provided
|
|
350
|
+
const hasModulesStep = !!endpoints.detectModules;
|
|
351
|
+
const STEPS = hasModulesStep
|
|
352
|
+
? ALL_STEPS
|
|
353
|
+
: ALL_STEPS.filter(s => s !== 'modules');
|
|
349
354
|
const ep = {
|
|
350
|
-
detectModules: endpoints.detectModules || '
|
|
355
|
+
detectModules: endpoints.detectModules || '',
|
|
351
356
|
testDb: endpoints.testDb || '/api/setup/test-db',
|
|
352
|
-
installModules: endpoints.installModules || '
|
|
357
|
+
installModules: endpoints.installModules || '',
|
|
353
358
|
install: endpoints.install || '/api/setup/install',
|
|
354
359
|
uploadJar: endpoints.uploadJar || '/api/setup/upload-jar',
|
|
355
|
-
wireModule: endpoints.wireModule || '
|
|
356
|
-
seed: endpoints.seed || '
|
|
360
|
+
wireModule: endpoints.wireModule || '',
|
|
361
|
+
seed: endpoints.seed || '',
|
|
357
362
|
};
|
|
358
363
|
// --- State ---
|
|
359
364
|
const [currentStep, setCurrentStep] = useState(0);
|
|
@@ -411,8 +416,12 @@ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNa
|
|
|
411
416
|
}
|
|
412
417
|
catch { }
|
|
413
418
|
}, [hydrated, persistState, currentStep, dialect, dbConfig, adminConfig, seedOptions, selectedModules]);
|
|
414
|
-
// --- Detect modules ---
|
|
419
|
+
// --- Detect modules (only if endpoint is provided) ---
|
|
415
420
|
useEffect(() => {
|
|
421
|
+
if (!ep.detectModules) {
|
|
422
|
+
setModulesDetected(true);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
416
425
|
fetch(ep.detectModules)
|
|
417
426
|
.then(r => r.json())
|
|
418
427
|
.then((data) => {
|
|
@@ -460,8 +469,10 @@ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNa
|
|
|
460
469
|
}
|
|
461
470
|
});
|
|
462
471
|
}, [availableModules]);
|
|
463
|
-
// --- Wire modules (load after installation success) ---
|
|
472
|
+
// --- Wire modules (load after installation success, only if endpoint provided) ---
|
|
464
473
|
const loadWireModules = useCallback(async () => {
|
|
474
|
+
if (!ep.wireModule)
|
|
475
|
+
return;
|
|
465
476
|
setWireLoading(true);
|
|
466
477
|
try {
|
|
467
478
|
const res = await fetch(ep.wireModule);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { writeEnvLocal } from './lib/env-writer';
|
|
|
5
5
|
export { DIALECT_INFO, ALL_DIALECTS } from './data/dialects';
|
|
6
6
|
export { MODULES, resolveModuleDependencies } from './data/module-definitions';
|
|
7
7
|
export { discoverNpmModules } from './lib/discover-modules';
|
|
8
|
+
export { loadSetupJson } from './lib/load-setup-json';
|
|
9
|
+
export type { SetupJson, SetupJsonRbac, SetupJsonSeed, SetupJsonCategory, SetupJsonPermission, SetupJsonRole } from './lib/load-setup-json';
|
|
8
10
|
export { createTestDbHandler } from './api/test-db.route';
|
|
9
11
|
export { createInstallHandler } from './api/install.route';
|
|
10
12
|
export { createStatusHandler } from './api/status.route';
|
|
@@ -13,6 +15,7 @@ export { createInstallModulesHandler } from './api/install-modules.route';
|
|
|
13
15
|
export { createReconfigHandlers } from './api/reconfig.route';
|
|
14
16
|
export { createUploadJarHandlers } from './api/upload-jar.route';
|
|
15
17
|
export { createWireModuleHandler } from './api/wire-module.route';
|
|
18
|
+
export { createSetupJsonHandler } from './api/upload-setup-json.route';
|
|
16
19
|
export { default as ReconfigPanel } from './components/ReconfigPanel';
|
|
17
20
|
export { default as SetupWizard } from './components/SetupWizard';
|
|
18
21
|
export { setupMenuContribution } from './lib/menu';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { DIALECT_INFO, ALL_DIALECTS } from './data/dialects';
|
|
|
10
10
|
export { MODULES, resolveModuleDependencies } from './data/module-definitions';
|
|
11
11
|
// Lib
|
|
12
12
|
export { discoverNpmModules } from './lib/discover-modules';
|
|
13
|
+
export { loadSetupJson } from './lib/load-setup-json';
|
|
13
14
|
// API route factories
|
|
14
15
|
export { createTestDbHandler } from './api/test-db.route';
|
|
15
16
|
export { createInstallHandler } from './api/install.route';
|
|
@@ -19,6 +20,7 @@ export { createInstallModulesHandler } from './api/install-modules.route';
|
|
|
19
20
|
export { createReconfigHandlers } from './api/reconfig.route';
|
|
20
21
|
export { createUploadJarHandlers } from './api/upload-jar.route';
|
|
21
22
|
export { createWireModuleHandler } from './api/wire-module.route';
|
|
23
|
+
export { createSetupJsonHandler } from './api/upload-setup-json.route';
|
|
22
24
|
// Components
|
|
23
25
|
export { default as ReconfigPanel } from './components/ReconfigPanel';
|
|
24
26
|
export { default as SetupWizard } from './components/SetupWizard';
|
package/dist/lib/compose-uri.js
CHANGED
|
@@ -8,7 +8,7 @@ export function composeDbUri(dialect, config) {
|
|
|
8
8
|
switch (dialect) {
|
|
9
9
|
case 'mongodb':
|
|
10
10
|
if (user && password)
|
|
11
|
-
return `mongodb://${eu}:${ep}@${host}:${port}/${name}`;
|
|
11
|
+
return `mongodb://${eu}:${ep}@${host}:${port}/${name}?authSource=admin`;
|
|
12
12
|
return `mongodb://${host}:${port}/${name}`;
|
|
13
13
|
case 'sqlite':
|
|
14
14
|
return `./data/${name}.db`;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { MostaSetupConfig } from '../types/index';
|
|
2
|
+
export interface SetupJsonCategory {
|
|
3
|
+
name: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
order?: number;
|
|
8
|
+
system?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SetupJsonPermission {
|
|
11
|
+
code: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SetupJsonRole {
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
system?: boolean;
|
|
20
|
+
permissions: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface SetupJsonRbac {
|
|
23
|
+
categories?: SetupJsonCategory[];
|
|
24
|
+
permissions?: SetupJsonPermission[];
|
|
25
|
+
roles?: SetupJsonRole[];
|
|
26
|
+
}
|
|
27
|
+
export interface SetupJsonSeed {
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
default?: boolean;
|
|
33
|
+
collection: string;
|
|
34
|
+
match?: string;
|
|
35
|
+
hashField?: string;
|
|
36
|
+
roleField?: string;
|
|
37
|
+
defaults?: Record<string, unknown>;
|
|
38
|
+
data: Record<string, unknown>[];
|
|
39
|
+
}
|
|
40
|
+
export interface SetupJson {
|
|
41
|
+
$schema?: string;
|
|
42
|
+
app: {
|
|
43
|
+
name: string;
|
|
44
|
+
port?: number;
|
|
45
|
+
dbNamePrefix?: string;
|
|
46
|
+
};
|
|
47
|
+
env?: Record<string, string>;
|
|
48
|
+
rbac?: SetupJsonRbac;
|
|
49
|
+
seeds?: SetupJsonSeed[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load a setup.json and return a MostaSetupConfig.
|
|
53
|
+
*
|
|
54
|
+
* @param source - File path (default: './setup.json'), or an already-parsed object.
|
|
55
|
+
* @param repoFactory - Optional factory to get a repository by collection name.
|
|
56
|
+
* Signature: `(collection: string) => Promise<{ upsert, create, findOne, count }>`
|
|
57
|
+
* If omitted, uses `@mostajs/orm` getRepository() at runtime.
|
|
58
|
+
*/
|
|
59
|
+
export declare function loadSetupJson(source?: string | SetupJson, repoFactory?: (collection: string) => Promise<GenericRepo>): Promise<MostaSetupConfig>;
|
|
60
|
+
type GenericRepo = {
|
|
61
|
+
upsert?: (where: Record<string, unknown>, data: Record<string, unknown>) => Promise<{
|
|
62
|
+
id: string;
|
|
63
|
+
}>;
|
|
64
|
+
create?: (data: Record<string, unknown>) => Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
}>;
|
|
67
|
+
findOne?: (where: Record<string, unknown>) => Promise<{
|
|
68
|
+
id: string;
|
|
69
|
+
} | null>;
|
|
70
|
+
count?: () => Promise<number>;
|
|
71
|
+
};
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @mostajs/setup — Declarative setup.json loader
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
//
|
|
4
|
+
// Reads a setup.json file and converts it into a MostaSetupConfig
|
|
5
|
+
// that can be passed to createInstallHandler() / runInstall().
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import { loadSetupJson } from '@mostajs/setup'
|
|
9
|
+
// const config = await loadSetupJson() // reads ./setup.json
|
|
10
|
+
// const config = await loadSetupJson('./my-setup.json') // custom path
|
|
11
|
+
// const config = await loadSetupJson(jsonObject) // already-parsed object
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
// ── Loader ───────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Load a setup.json and return a MostaSetupConfig.
|
|
17
|
+
*
|
|
18
|
+
* @param source - File path (default: './setup.json'), or an already-parsed object.
|
|
19
|
+
* @param repoFactory - Optional factory to get a repository by collection name.
|
|
20
|
+
* Signature: `(collection: string) => Promise<{ upsert, create, findOne, count }>`
|
|
21
|
+
* If omitted, uses `@mostajs/orm` getRepository() at runtime.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadSetupJson(source, repoFactory) {
|
|
24
|
+
const json = typeof source === 'object' && source !== null
|
|
25
|
+
? source
|
|
26
|
+
: readJsonFile(typeof source === 'string' ? source : './setup.json');
|
|
27
|
+
validate(json);
|
|
28
|
+
return buildConfig(json, repoFactory);
|
|
29
|
+
}
|
|
30
|
+
function readJsonFile(filePath) {
|
|
31
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
32
|
+
if (!fs.existsSync(resolved)) {
|
|
33
|
+
throw new Error(`setup.json not found at ${resolved}`);
|
|
34
|
+
}
|
|
35
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
function validate(json) {
|
|
39
|
+
if (!json.app?.name) {
|
|
40
|
+
throw new Error('setup.json: "app.name" is required');
|
|
41
|
+
}
|
|
42
|
+
// Validate RBAC cross-references
|
|
43
|
+
if (json.rbac) {
|
|
44
|
+
const categoryNames = new Set((json.rbac.categories ?? []).map(c => c.name));
|
|
45
|
+
const permCodes = new Set((json.rbac.permissions ?? []).map(p => p.code));
|
|
46
|
+
for (const perm of json.rbac.permissions ?? []) {
|
|
47
|
+
if (categoryNames.size > 0 && !categoryNames.has(perm.category)) {
|
|
48
|
+
throw new Error(`setup.json: permission "${perm.code}" references unknown category "${perm.category}"`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const role of json.rbac.roles ?? []) {
|
|
52
|
+
for (const code of role.permissions) {
|
|
53
|
+
if (code !== '*' && permCodes.size > 0 && !permCodes.has(code)) {
|
|
54
|
+
throw new Error(`setup.json: role "${role.name}" references unknown permission "${code}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function buildConfig(json, repoFactory) {
|
|
61
|
+
const config = {
|
|
62
|
+
appName: json.app.name,
|
|
63
|
+
defaultPort: json.app.port,
|
|
64
|
+
extraEnvVars: json.env ? { ...json.env } : undefined,
|
|
65
|
+
};
|
|
66
|
+
// ── seedRBAC ───────────────────────────────────────────
|
|
67
|
+
if (json.rbac) {
|
|
68
|
+
config.seedRBAC = async () => {
|
|
69
|
+
const getRepo = repoFactory ?? defaultRepoFactory;
|
|
70
|
+
// 1. Upsert categories
|
|
71
|
+
if (json.rbac.categories?.length) {
|
|
72
|
+
const catRepo = await getRepo('permissionCategory');
|
|
73
|
+
for (const cat of json.rbac.categories) {
|
|
74
|
+
await catRepo.upsert({ name: cat.name }, { name: cat.name, label: cat.label, description: cat.description ?? '', icon: cat.icon ?? '', order: cat.order ?? 0, system: cat.system ?? true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 2. Upsert permissions — build code→ID map
|
|
78
|
+
const permissionMap = {};
|
|
79
|
+
if (json.rbac.permissions?.length) {
|
|
80
|
+
const permRepo = await getRepo('permission');
|
|
81
|
+
for (const pDef of json.rbac.permissions) {
|
|
82
|
+
const displayName = pDef.name ?? pDef.code;
|
|
83
|
+
const perm = await permRepo.upsert({ name: displayName }, { name: displayName, description: pDef.description, category: pDef.category });
|
|
84
|
+
permissionMap[pDef.code] = perm.id;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 3. Upsert roles with resolved permission IDs
|
|
88
|
+
if (json.rbac.roles?.length) {
|
|
89
|
+
const roleRepo = await getRepo('role');
|
|
90
|
+
const allPermIds = Object.values(permissionMap);
|
|
91
|
+
for (const roleDef of json.rbac.roles) {
|
|
92
|
+
const permissionIds = roleDef.permissions.includes('*')
|
|
93
|
+
? allPermIds
|
|
94
|
+
: roleDef.permissions.map(code => permissionMap[code]).filter(Boolean);
|
|
95
|
+
await roleRepo.upsert({ name: roleDef.name }, { name: roleDef.name, description: roleDef.description ?? '', permissions: permissionIds });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ── optionalSeeds ──────────────────────────────────────
|
|
101
|
+
if (json.seeds?.length) {
|
|
102
|
+
config.optionalSeeds = json.seeds.map(seedDef => buildSeedDefinition(seedDef, repoFactory));
|
|
103
|
+
}
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
function buildSeedDefinition(seedDef, repoFactory) {
|
|
107
|
+
return {
|
|
108
|
+
key: seedDef.key,
|
|
109
|
+
label: seedDef.label,
|
|
110
|
+
description: seedDef.description ?? '',
|
|
111
|
+
icon: seedDef.icon,
|
|
112
|
+
default: seedDef.default,
|
|
113
|
+
run: async () => {
|
|
114
|
+
const getRepo = repoFactory ?? defaultRepoFactory;
|
|
115
|
+
const repo = await getRepo(seedDef.collection);
|
|
116
|
+
for (const rawItem of seedDef.data) {
|
|
117
|
+
const item = { ...(seedDef.defaults ?? {}), ...rawItem };
|
|
118
|
+
// Hash field if configured (e.g. password)
|
|
119
|
+
if (seedDef.hashField && item[seedDef.hashField]) {
|
|
120
|
+
const bcryptModule = await import('bcryptjs');
|
|
121
|
+
const bcrypt = bcryptModule.default || bcryptModule;
|
|
122
|
+
item[seedDef.hashField] = await bcrypt.hash(String(item[seedDef.hashField]), 12);
|
|
123
|
+
}
|
|
124
|
+
// Resolve role name → role ID if configured
|
|
125
|
+
if (seedDef.roleField && item[seedDef.roleField]) {
|
|
126
|
+
const roleRepo = await getRepo('role');
|
|
127
|
+
const roleName = String(item[seedDef.roleField]);
|
|
128
|
+
const role = await roleRepo.findOne({ name: roleName });
|
|
129
|
+
if (role) {
|
|
130
|
+
item.roles = [role.id];
|
|
131
|
+
}
|
|
132
|
+
delete item[seedDef.roleField];
|
|
133
|
+
}
|
|
134
|
+
// Upsert (idempotent) or create
|
|
135
|
+
if (seedDef.match && repo.upsert) {
|
|
136
|
+
const matchValue = item[seedDef.match];
|
|
137
|
+
await repo.upsert({ [seedDef.match]: matchValue }, item);
|
|
138
|
+
}
|
|
139
|
+
else if (repo.create) {
|
|
140
|
+
// Skip if already exists (check match field if available)
|
|
141
|
+
if (seedDef.match && repo.findOne) {
|
|
142
|
+
const existing = await repo.findOne({ [seedDef.match]: item[seedDef.match] });
|
|
143
|
+
if (existing)
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
await repo.create(item);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function defaultRepoFactory(collection) {
|
|
153
|
+
// Try to import a getRepository from @mostajs/orm if available
|
|
154
|
+
try {
|
|
155
|
+
const orm = await import('@mostajs/orm');
|
|
156
|
+
if ('getRepository' in orm && typeof orm.getRepository === 'function') {
|
|
157
|
+
return await orm.getRepository(collection);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* fallback below */ }
|
|
161
|
+
throw new Error(`No repository factory provided for collection "${collection}". ` +
|
|
162
|
+
`Pass a repoFactory argument to loadSetupJson().`);
|
|
163
|
+
}
|
package/dist/lib/setup.js
CHANGED
|
@@ -69,7 +69,8 @@ export async function runInstall(installConfig, setupConfig) {
|
|
|
69
69
|
}
|
|
70
70
|
// 5. Create admin user
|
|
71
71
|
if (setupConfig.createAdmin) {
|
|
72
|
-
const
|
|
72
|
+
const bcryptModule = await import('bcryptjs');
|
|
73
|
+
const bcrypt = bcryptModule.default || bcryptModule;
|
|
73
74
|
const hashedPassword = await bcrypt.hash(installConfig.admin.password, 12);
|
|
74
75
|
await setupConfig.createAdmin({
|
|
75
76
|
email: installConfig.admin.email,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/setup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,10 +67,13 @@
|
|
|
67
67
|
"types": "./dist/register.d.ts",
|
|
68
68
|
"import": "./dist/register.js",
|
|
69
69
|
"default": "./dist/register.js"
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
|
+
"./schemas/*": "./schemas/*"
|
|
71
72
|
},
|
|
72
73
|
"files": [
|
|
73
74
|
"dist",
|
|
75
|
+
"schemas",
|
|
76
|
+
"cli",
|
|
74
77
|
"wire.json",
|
|
75
78
|
"setup.wire.json",
|
|
76
79
|
"LICENSE",
|
|
@@ -93,6 +96,9 @@
|
|
|
93
96
|
"engines": {
|
|
94
97
|
"node": ">=18.0.0"
|
|
95
98
|
},
|
|
99
|
+
"bin": {
|
|
100
|
+
"mosta-setup": "./dist/cli/init.js"
|
|
101
|
+
},
|
|
96
102
|
"scripts": {
|
|
97
103
|
"build": "tsc",
|
|
98
104
|
"prepublishOnly": "npm run build"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mostajs.dev/schemas/setup.v1.json",
|
|
4
|
+
"title": "MostaJS Setup Configuration",
|
|
5
|
+
"description": "Declarative setup manifest for @mostajs/setup — defines app info, RBAC, seeds, and env vars for the install wizard.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["app"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "JSON Schema reference for IDE autocompletion"
|
|
13
|
+
},
|
|
14
|
+
"app": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"description": "Application metadata",
|
|
17
|
+
"required": ["name"],
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Application name displayed in the setup wizard",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"examples": ["SecuAccessPro", "MostaGare", "MonApp"]
|
|
25
|
+
},
|
|
26
|
+
"port": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"description": "Default HTTP port",
|
|
29
|
+
"default": 3000,
|
|
30
|
+
"minimum": 1,
|
|
31
|
+
"maximum": 65535
|
|
32
|
+
},
|
|
33
|
+
"dbNamePrefix": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Default database name prefix used in the wizard",
|
|
36
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
37
|
+
"examples": ["secuaccessdb", "mostagaredb"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"env": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"description": "Extra environment variables written to .env.local during install",
|
|
44
|
+
"additionalProperties": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"examples": [
|
|
48
|
+
{ "MOSTAJS_MODULES": "orm,auth,audit,rbac,settings,setup" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"rbac": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"description": "Role-Based Access Control definitions seeded at install time",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"properties": {
|
|
56
|
+
"categories": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"description": "Permission categories (grouping)",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["name", "label"],
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"properties": {
|
|
64
|
+
"name": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Unique key (e.g. 'admin', 'client')",
|
|
67
|
+
"pattern": "^[a-z][a-z0-9_]*$"
|
|
68
|
+
},
|
|
69
|
+
"label": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Display label"
|
|
72
|
+
},
|
|
73
|
+
"description": {
|
|
74
|
+
"type": "string"
|
|
75
|
+
},
|
|
76
|
+
"icon": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Lucide icon name (e.g. 'Settings', 'Users')"
|
|
79
|
+
},
|
|
80
|
+
"order": {
|
|
81
|
+
"type": "integer",
|
|
82
|
+
"minimum": 0,
|
|
83
|
+
"description": "Display order"
|
|
84
|
+
},
|
|
85
|
+
"system": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"default": true,
|
|
88
|
+
"description": "Whether this category is system-defined (not user-deletable)"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"permissions": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"description": "Permission definitions",
|
|
96
|
+
"items": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"required": ["code", "description", "category"],
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"properties": {
|
|
101
|
+
"code": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Unique permission code (e.g. 'client:view')",
|
|
104
|
+
"pattern": "^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$"
|
|
105
|
+
},
|
|
106
|
+
"name": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "Display name (defaults to code if omitted)"
|
|
109
|
+
},
|
|
110
|
+
"description": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Human-readable description"
|
|
113
|
+
},
|
|
114
|
+
"category": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Category name reference"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"roles": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"description": "Role definitions with permission assignments",
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"required": ["name", "permissions"],
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"properties": {
|
|
129
|
+
"name": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Unique role name (e.g. 'admin', 'agent_accueil')",
|
|
132
|
+
"pattern": "^[a-z][a-z0-9_]*$"
|
|
133
|
+
},
|
|
134
|
+
"description": {
|
|
135
|
+
"type": "string"
|
|
136
|
+
},
|
|
137
|
+
"system": {
|
|
138
|
+
"type": "boolean",
|
|
139
|
+
"default": true,
|
|
140
|
+
"description": "Whether this role is system-defined"
|
|
141
|
+
},
|
|
142
|
+
"permissions": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"description": "Permission codes assigned to this role. Use ['*'] for all permissions.",
|
|
145
|
+
"items": {
|
|
146
|
+
"type": "string"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"seeds": {
|
|
155
|
+
"type": "array",
|
|
156
|
+
"description": "Optional seed datasets shown as checkboxes in the install wizard",
|
|
157
|
+
"items": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["key", "label", "collection", "data"],
|
|
160
|
+
"additionalProperties": false,
|
|
161
|
+
"properties": {
|
|
162
|
+
"key": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "Unique seed identifier",
|
|
165
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
|
|
166
|
+
},
|
|
167
|
+
"label": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"description": "Label shown in the wizard checkbox"
|
|
170
|
+
},
|
|
171
|
+
"description": {
|
|
172
|
+
"type": "string",
|
|
173
|
+
"description": "Description shown below the label"
|
|
174
|
+
},
|
|
175
|
+
"icon": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": "Lucide icon name"
|
|
178
|
+
},
|
|
179
|
+
"default": {
|
|
180
|
+
"type": "boolean",
|
|
181
|
+
"default": false,
|
|
182
|
+
"description": "Whether this seed is checked by default"
|
|
183
|
+
},
|
|
184
|
+
"collection": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Target collection/table name (must match a registered schema)"
|
|
187
|
+
},
|
|
188
|
+
"match": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": "Field used for upsert matching (idempotent seeding). If omitted, uses 'create' (non-idempotent)."
|
|
191
|
+
},
|
|
192
|
+
"hashField": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"description": "Field to bcrypt-hash before insert (e.g. 'password')"
|
|
195
|
+
},
|
|
196
|
+
"roleField": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Field containing a role name — resolved to role ID at runtime"
|
|
199
|
+
},
|
|
200
|
+
"defaults": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"description": "Default values merged into every data item",
|
|
203
|
+
"additionalProperties": true
|
|
204
|
+
},
|
|
205
|
+
"data": {
|
|
206
|
+
"type": "array",
|
|
207
|
+
"description": "Array of records to seed",
|
|
208
|
+
"items": {
|
|
209
|
+
"type": "object",
|
|
210
|
+
"additionalProperties": true
|
|
211
|
+
},
|
|
212
|
+
"minItems": 1
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|