@mostajs/project-life
v0.1.3
Published
Project lifecycle management (create, pause, resume, delete) for @mostajs
Downloads
215
Readme
@mostajs/project-life
Project lifecycle management (create, pause, resume, delete) pour les hôtes mostajs.
Auteur : Dr Hamid MADANI [email protected] License : AGPL-3.0-or-later
Principe
project-life gère la table de référence des projets d'un hôte multi-tenant : enregistrement, pause, reprise, suppression. C'est un module système (au sens : ses données vivent dans la base système, pas dans la base métier d'un projet).
Distinction claire avec les autres modules de l'écosystème projet :
| Module | Rôle |
|--------|------|
| @mostajs/project-life | Métadonnées projets (qui existent, leur état, leur config) — table système |
| @mostajs/mproject | Cycle de vie des ressources d'un projet (connexion DB isolée, EntityService dédié) — createIsolatedDialect |
| @mostajs/replicator | Réplication des données entre projets — connexions isolées par stream |
project-life est le « registre civil » des projets : il sait qu'ils existent. mproject est le « cadastre » : il sait où ils habitent et leur ouvre la porte.
Install
npm install @mostajs/project-life
# peer deps :
npm install @mostajs/data-plug @mostajs/mprojectUsage
1. Enregistrer le schema système au bootstrap
project-life expose son EntitySchema (ProjectSchema) que l'hôte enregistre auprès du registre global.
import { registerSchemas } from '@mostajs/data-plug'
import { ProjectSchema } from '@mostajs/project-life'
registerSchemas([ProjectSchema])2. Provisionner un nouveau projet
import { provisionProject } from '@mostajs/project-life/server'
import { getSystemDialect } from '@mostajs/data-plug'
import { ProjectManager } from '@mostajs/mproject'
const dialect = await getSystemDialect() // dialect SYSTÈME
const pm = new ProjectManager()
const result = await provisionProject(dialect, pm, accountId, {
name: 'orphan-care',
slug: 'orphan-care',
dialect: 'postgres',
uri: 'postgresql://hmd:***@127.0.0.1:5432/orphan_care_db',
schemas: [/* EntitySchema[] du projet */],
transports: ['rest', 'graphql'],
poolSize: 5,
})
if (result.ok) {
console.log('Provisioned as mproject:', result.mprojectName)
}3. Pause / Resume
import { pauseProject, resumeProject } from '@mostajs/project-life/server'
// Pause : ferme la connexion isolée du projet, marque status='paused'
await pauseProject(dialect, pm, projectId)
// Resume : ré-ouvre la connexion + status='active'
// (encKey requis si l'URI est chiffrée — cf. encryptUri ci-dessous)
await resumeProject(dialect, pm, projectId, encKey)4. Déprovisionner
import { deprovisionProject } from '@mostajs/project-life/server'
await deprovisionProject(dialect, pm, projectId)
// → ferme la connexion isolée, supprime la row dans la table projects5. Bootstrap au démarrage de l'hôte
Au démarrage, ré-ouvrir toutes les connexions des projets actifs (typiquement après un restart pm2) :
import { bootAllProjects } from '@mostajs/project-life/server'
await bootAllProjects(dialect, pm, encKey)
// → pour chaque project status='active', ouvre une connexion isolée via pm6. Routes Fastify (optionnel — handlers prêts à l'emploi)
import { createProjectHandlers } from '@mostajs/project-life/server'
const handlers = createProjectHandlers(dialect, pm, encKey)
app.get('/api/projects', handlers.list)
app.post('/api/projects', handlers.create)
app.post('/api/projects/:id/pause', handlers.pause)
app.post('/api/projects/:id/resume', handlers.resume)
app.delete('/api/projects/:id', handlers.delete)7. Chiffrement des URIs (optionnel)
Pour les déploiements où les credentials DB ne doivent pas être lisibles en clair dans la base système :
import { encryptUri, decryptUri, generateEncryptionKey } from '@mostajs/project-life/server'
// Génération initiale (à mettre en .env : PROJECT_ENCRYPTION_KEY=<key>)
const key = generateEncryptionKey()
// À l'enregistrement
const encryptedUri = encryptUri('postgresql://hmd:***@host/db', key)
// Au resume / boot
const plainUri = decryptUri(encryptedUri, key)L'env var PROJECT_ENCRYPTION_KEY est lue automatiquement par getEncryptionKey() si pas de clé passée explicitement.
API
Server-side (@mostajs/project-life/server)
| Fonction | Signature |
|----------|-----------|
| provisionProject | (dialect, pm, accountId, config: ProjectConfig) → Promise<ProvisionResult> |
| deprovisionProject | (dialect, pm, projectId) → Promise<{ok, error?}> |
| pauseProject | (dialect, pm, projectId) → Promise<{ok, error?}> |
| resumeProject | (dialect, pm, projectId, encKey?) → Promise<{ok, error?}> |
| bootAllProjects | (dialect, pm, encKey?) → Promise<void> |
| getProjectRepo | (dialect) → BaseRepository<ProjectDTO> |
| createProjectHandlers | (dialect, pm, encKey?) → { list, create, pause, resume, delete } |
| encryptUri / decryptUri | (uri \| encrypted, key?) → string |
| generateEncryptionKey | () → string (base64) |
Client-safe (@mostajs/project-life)
export type { ProjectDTO, ProjectConfig, ProvisionResult, ProjectStatus }
export { ProjectSchema, moduleInfo }Schema ProjectDTO
| Champ | Type | Description |
|-------|------|-------------|
| id | string | UUID |
| account | string | FK → Account propriétaire |
| name | string | Nom affichable |
| slug | string | Identifiant URL-safe (unique par account) |
| dialect | string | postgres, mysql, mongodb, sqlite, … |
| uri | string | Connexion DB (en clair ou chiffré via encryptUri) |
| schemas | any[] | EntitySchema[] des entités du projet |
| transports | string[] | Ex : ['rest', 'graphql', 'mcp'] |
| poolSize | number | Taille du pool DB |
| status | 'active' \| 'paused' \| 'error' | État courant |
| error? | string | Dernière erreur si status='error' |
| mprojectName? | string | Nom interne ProjectManager (rempli au provisioning) |
| createdAt, updatedAt | string | ISO 8601 |
Architecture
┌─────────────────────────────┐
│ @mostajs/project-life │
│ (table système — registre) │
│ │
│ ProjectDTO row │
│ { id, dialect, uri, … } │
└──────────────┬──────────────┘
│ provisionProject()
▼
┌─────────────────────────────┐
│ @mostajs/mproject │
│ (ProjectManager — runtime) │
│ │
│ createIsolatedDialect │
│ EntityService dédié │
│ │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ @mostajs/replicator │
│ (sync inter-projets) │
└─────────────────────────────┘project-life ne touche pas aux ressources d'un projet (c'est mproject). Il garde juste la trace que le projet existe.
Changelog
v0.1.3 — 2026-05-04 — Découplage @mostajs/orm via façade data-plug + WeakMap repo
Étape 3 du chantier « system dialect séparé » — applique deux fix qui se combinent.
1. Migration @mostajs/orm → @mostajs/data-plug (façade)
Conformément au principe « les modules @mostajs passent par data-plug, jamais hardcoder un dialect ou importer @mostajs/orm directement », project-life ne dépend plus de @mostajs/orm en peerDependency. Tous les imports de production passent désormais par @mostajs/data-plug v1.2.4.
6 fichiers migrés :
| Fichier | Symboles |
|---------|----------|
| src/api/projects.route.ts | IDialect |
| src/lib/boot.ts | IDialect |
| src/lib/module-info.ts | EntitySchema |
| src/lib/project-factory.ts | BaseRepository + IDialect |
| src/lib/provisioning.ts | IDialect |
| src/schemas/project.schema.ts | EntitySchema |
| package.json | peerDep orm → data-plug ^1.2.4 |
@mostajs/orm reste en devDependency uniquement (cohérence pour test-unit éventuel).
2. WeakMap dans project-factory.ts
// Avant — capture la référence du PREMIER dialect, ignore les suivants
let repo: BaseRepository<ProjectDTO> | null = null
// Après — keyed par identité du dialect
const cache = new WeakMap<IDialect, BaseRepository<ProjectDTO>>()Évite que le repo capture la référence du PREMIER dialect passé et ignore tous les suivants. Lorsque /api/change-dialect (ou rotation système ↔ métier) modifie le dialect courant, le cache miss force la reconstruction du repo avec la nouvelle instance dialect — au lieu de réutiliser un repo pointant vers une connexion morte.
resetProjectRepo() conservé en no-op pour rétro-compat (la WeakMap auto-libère naturellement les entrées dont le dialect n'est plus référencé).
Bump
0.1.2 → 0.1.3 (patch — découplage interne, signatures publiques inchangées).
v0.1.2 et antérieurs
Versions initiales de provisioning + encryption + bootstrap. Pas de release notes archivées.
License
AGPL-3.0-or-later — usage libre tant que le code dérivé reste open-source.
Licence commerciale disponible : [email protected]. Pricing par projet, pas par seat.
— (c) 2026 Dr Hamid MADANI <[email protected]>
