@mostajs/data-plug
v1.2.5
Published
Data access plug — interchangeable backend (ORM direct, REST proxy, …) with multi-dialect registry + auto-register apikey via @mostajs/auth-flow (MOSTA_AUTH_FLOW_URL distinct, forceRefresh, clearCachedApiKey) + system dialect séparé du singleton métier (b
Maintainers
Readme
@mostajs/octoswitcher
Data access switcher — ORM direct or NET transport, one interface, zero config in modules.
Principe
MOSTA_DATA=orm → @mostajs/orm → SQL direct (SQLite, PostgreSQL, Oracle, MSSQL, etc.)
MOSTA_DATA=net → @mostajs/net → Transport distant (REST, GraphQL, gRPC, WS, MCP)Les modules @mostajs (auth, rbac, audit, settings, ticketing, secu) appellent getDialect() depuis octoswitcher — ils ne savent pas si les données viennent d'une base locale ou d'un serveur distant.
Installation
npm install @mostajs/octoswitcherUsage dans un module
import { getDialect } from '@mostajs/octoswitcher'
import { UserRepository } from './repositories/user.repository.js'
const dialect = await getDialect()
const repo = new UserRepository(dialect)
const users = await repo.findAll()Configuration (dans l'app, pas dans le module)
Mode ORM (accès direct base de données)
MOSTA_DATA=orm
DB_DIALECT=sqlite
SGBD_URI=./data/app.dbMode NET (accès distant via transport)
MOSTA_DATA=net
MOSTA_NET_URL=https://mcp.amia.fr/astro_08/
MOSTA_NET_TRANSPORT=restAPI
getDialect(): Promise<IDataDialect>
Retourne le dialect singleton — ORM ou NET selon MOSTA_DATA.
getDataMode(): DataMode
Retourne 'orm' ou 'net'.
isNetMode(): boolean
Raccourci pour getDataMode() === 'net'.
isOrmMode(): boolean
Raccourci pour getDataMode() === 'orm'.
setDialect(dialect): void
Injecte un dialect externe (pour les tests).
resetDialect(): void
Réinitialise le singleton (pour les tests).
getSystemDialect(): Promise<IDataDialect> (v1.2.2+)
Retourne le dialect système (apikeys, RBAC, audit, plans, payments, project-life). Stable au runtime, jamais muté par les routes admin métier. Si bootstrapSystemDialect() n'a pas été appelé, fallback transparent vers le singleton métier.
bootstrapSystemDialect(): Promise<IDataDialect> (v1.2.2+)
Initialise le dialect système au démarrage de l'app. Lit MOSTA_SYSTEM_DIALECT + MOSTA_SYSTEM_URI ; si présents → ouvre une connexion isolée dédiée via openIsolatedDialect, sinon alias singleton métier (rétro-compat). Idempotent.
setSystemDialect(dialect): void (v1.2.2+)
Injecte un dialect système externe (testing / custom implementation). Pendant naturel de setDialect() côté métier.
resetSystemDialect(): void (v1.2.2+)
Réinitialise le dialect système (testing / teardown). Ne ferme PAS la connexion physique.
Architecture
App (.env.local)
├── MOSTA_DATA=net + MOSTA_NET_URL=...
│
├── @mostajs/auth → @mostajs/rbac → octoswitcher → getDialect()
├── @mostajs/rbac → octoswitcher → getDialect()
├── @mostajs/audit → octoswitcher → getDialect()
├── @mostajs/settings → octoswitcher → getDialect()
├── @mostajs/ticketing → octoswitcher → getDialect()
└── @mostajs/secu → octoswitcher → getDialect()
│
┌─────────┴─────────┐
│ │
MOSTA_DATA=orm MOSTA_DATA=net
@mostajs/orm @mostajs/net
SQL direct REST/GraphQL/gRPCModules migrés
| Module | Avant | Après | |--------|-------|-------| | rbac | 212 lignes (data-mode.ts + NET factories) | 106 lignes | | audit | 130 lignes (data-mode.ts + NET factories) | 41 lignes | | settings | 119 lignes (data-mode.ts + NET factories) | 41 lignes | | ticketing | hardcodé ORM | 1 import changé | | secu | hardcodé ORM | 1 import changé |
Release Notes
v1.2.5 — 2026-05-04 — bootstrapSystemDialect passe schemaStrategy au dialect isolé
Bug fix (régression du chantier system dialect, détectée au déploiement test sur amia).
Symptôme
Au premier boot d'octonet-mcp avec MOSTA_SYSTEM_DIALECT + MOSTA_SYSTEM_URI définis (mode multi-base), les tables système (RBAC permission_categories, roles, users, …, api_keys, scopes, scope_values) n'étaient jamais créées dans la base système. Logs :
⚠ RBAC bootstrap skipped: relation "permission_categories" does not exist
⚠ Apikey scope registration skipped: relation "api_key_scopes" does not existCause
bootstrapSystemDialect() appelait openIsolatedDialect({dialect, uri}) sans schemaStrategy. Tous les dialects implémentent initSchema(schemas) ainsi :
async initSchema(schemas) {
const strategy = this.config?.schemaStrategy ?? 'none';
// si strategy === 'none' → no-op
// si strategy === 'update'/'create' → crée/migre tables
}→ Sans schemaStrategy, strategy='none' par défaut → initSchema no-op → tables jamais créées même quand bootstrapRbac ou registerScope les demandent.
Fix
// data-plug v1.2.5
export async function bootstrapSystemDialect() {
// …
const schemaStrategy = getEnv('MOSTA_SYSTEM_SCHEMA_STRATEGY', 'update');
const sys = await openIsolatedDialect({ dialect, uri, schemaStrategy });
// …
}Override possible via env var MOSTA_SYSTEM_SCHEMA_STRATEGY (default 'update' — idempotent : crée si absent, sinon migration douce).
Bump
1.2.4 → 1.2.5 (patch — bug fix sans changement d'API).
v1.2.4 — 2026-05-03 — Façade ORM complète (registry helpers)
Complète la façade v1.2.3 en ré-exportant aussi les registry helpers (registre global des schemas). Permet aux modules consumers de résoudre des entités par nom à l'exécution sans import('@mostajs/orm') direct.
export {
registerSchema, registerSchemas,
getSchema, getSchemaByCollection,
getAllSchemas, getEntityNames,
hasSchema, validateSchemas, clearRegistry,
} from '@mostajs/orm'Bump 1.2.3 → 1.2.4 (patch — additif pur).
v1.2.3 — 2026-05-03 — Façade ORM (re-exports)
data-plug ré-exporte les primitives @mostajs/orm consommées par les modules applicatifs (api-keys, rbac, audit, project-life, payment, subscriptions-plan, …). Conformément au principe « les modules @mostajs passent par data-plug, jamais hardcoder un dialect ou importer @mostajs/orm directement », data-plug devient le point d'entrée unique data-access.
Exports ajoutés (rétro-compat 100 %, additif pur)
// Class
export { BaseRepository, normalizeDoc, normalizeDocs } from '@mostajs/orm'
// Types
export type {
IDialect, EntitySchema, FilterQuery, QueryOptions, IRepository,
FieldDef, FieldType, RelationDef, RelationType, IndexDef,
} from '@mostajs/orm'Migration des modules consumers
// Avant (couplage dur à orm)
import { BaseRepository } from '@mostajs/orm'
import type { IDialect, EntitySchema } from '@mostajs/orm'
// Après (façade data-plug)
import { BaseRepository } from '@mostajs/data-plug'
import type { IDialect, EntitySchema } from '@mostajs/data-plug'Bump
1.2.2 → 1.2.3 (patch — pure addition d'exports).
v1.2.2 — 2026-05-03 — System dialect séparé du singleton métier
Introduit getSystemDialect / bootstrapSystemDialect / setSystemDialect / resetSystemDialect pour découpler les modules SYSTÈME (apikeys, RBAC, audit, plans, payments, project-life) du singleton MÉTIER mutable.
Motivation
Le singleton getDialect() est mutable au runtime via /api/change-dialect, /api/reload-config, /api/reconnect — légitime côté métier. Mais les modules système (qui hébergent les apikeys, scopes, RBAC users, plans de souscription, audit) doivent vivre dans une base STABLE qui ne suit pas ces mutations, sinon les apikeys deviennent introuvables après un changement de dialect métier.
API ajoutée (rétro-compat 100 %)
getSystemDialect()— retourne le dialect système. Fallback singleton métier si non bootstrappé.bootstrapSystemDialect()— initialise au démarrage. LitMOSTA_SYSTEM_DIALECT+MOSTA_SYSTEM_URI:- présents → connexion isolée dédiée via
openIsolatedDialect(prod multi-base) - absents → alias singleton métier (rétro-compat mono-base)
- présents → connexion isolée dédiée via
setSystemDialect(d)— injection (testing / custom implementation, pendant naturel desetDialectcôté métier).resetSystemDialect()— reset (testing / teardown).
Configuration cible (prod multi-base)
# Métier (mutable via IHM admin) :
DB_DIALECT=postgres
SGBD_URI=postgresql://hmd:***@127.0.0.1:5432/octonet_business
# Système (stable, jamais touché par /api/change-dialect) :
MOSTA_SYSTEM_DIALECT=postgres
MOSTA_SYSTEM_URI=postgresql://hmd:***@127.0.0.1:5432/octonet_systemTests
6 / 6 passent (tests-scripts/test-system-dialect.mjs) :
| # | Assertion |
|---|-----------|
| T1 | fallback alias business |
| T2 | getSystemDialect() returns bootstrapped |
| T3 | system distinct from business |
| T4 | system survives business disconnect() |
| T5 | reset then bootstrap re-fallbacks |
| T6 | bootstrap idempotent |
Bump
1.2.1 → 1.2.2 (minor — API additive uniquement, aucun breaking change).
v1.2.1 — 2026-05-03 — Trajet 2 (apikey périmée) + URL découplée
3 ajouts purement techniques pour clôturer le flow auto-register E2E. Cohérent avec l'architecture Octonet=cité, Octocloud=maire (cf. memory/reference_octonet_octocloud_architecture.md).
1. MOSTA_AUTH_FLOW_URL env distinct
Le device flow tape sur Octocloud (le maire qui distribue les clés), PAS sur Octonet (la cité qui héberge le data plane). Si MOSTA_AUTH_FLOW_URL est défini, on l'utilise ; sinon fallback MOSTA_NET_URL pour rétro-compat v1.2.0.
# Configuration cible recommandée v1.2.1+ :
MOSTA_AUTH_FLOW_URL=https://octocloud.amia.fr # device flow (le maire)
MOSTA_NET_URL=https://octonet.amia.fr # data plane (la cité)
# Rétro-compat v1.2.0 (un seul endpoint pour tout) :
MOSTA_NET_URL=https://octocloud.amia.fr # auth + data via même endpoint2. EnsureApiKeyOptions.forceRefresh
Cas d'usage trajet 2 : @mostajs/net-client-js détecte 401 sur un call data plane → callback onApiKeyInvalid → appel à ensureApiKey({ forceRefresh: true }) qui invalide le cache local et redéclenche le device flow vers Octocloud.
import { ensureApiKey } from '@mostajs/data-plug'
netClient.onApiKeyInvalid(async () => {
return await ensureApiKey({ forceRefresh: true })
})Comportement avec forceRefresh: true :
- Ignore le cache
~/.config/<host>/auth.json - Ignore
MOSTA_NET_API_KEYenv (peut être elle-même périmée) - Ignore
MOSTA_NO_AUTOREGISTER(caller veut absolument refresh, il a déjà eu un 401) - Appelle
flow.refresh()qui clear le store + lance un nouveau device flow
Note CI / non-interactif : si
forceRefresh: true+ environnement non-interactif (pas de browser pour approve), le flow timeout naturellement. Le caller doit gérer ce cas.
3. clearCachedApiKey({ host? }) helper exporté
Supprime l'apikey cached localement (~/.config/<host>/auth.json). No-op si pas de cache existant ou si @mostajs/auth-flow non installé.
import { clearCachedApiKey } from '@mostajs/data-plug'
// Sign-out explicite client (orphan-care --logout)
await clearCachedApiKey({ host: 'orphan-care' })
// Le prochain ensureApiKey() redéclenchera un device flowCas d'usage :
- Sign-out explicite côté client
- Outils admin qui purgent les apikeys locales
- Tests qui doivent partir d'un état propre
Tests
11 nouveaux assertions dans tests-scripts/test-auto-register.ts (12 → 23 assertions) :
- T2.A/B/C —
MOSTA_AUTH_FLOW_URLpriorité + fallback NET_URL + throw si aucun - T3.A —
forceRefreshignoreMOSTA_NET_API_KEYenv - T3.B —
forceRefreshignoreMOSTA_NO_AUTOREGISTER+ cache - T4 —
clearCachedApiKey()no-op tolérant
Rétro-compat
100% — apps existantes en v1.2.0 (qui utilisent uniquement MOSTA_NET_URL) continuent à fonctionner sans changement. Les 3 ajouts sont opt-in (env var ou option de fonction).
Plan détaillé
Cf. docs/PLAN-V1.2.1.md — checklist d'exécution + tests + commit format.
Récap technique fichier-par-fichier
src/auto-register.ts (modifié) :
MOSTA_AUTH_FLOW_URLenv distinct (fallbackMOSTA_NET_URLpour rétro-compat v1.2.0). Le device flow tape sur Octocloud (le maire qui distribue les clés), PAS sur Octonet (la cité qui héberge le data plane).EnsureApiKeyOptions.forceRefresh— invalide cache + relance fresh device flow.- Cas d'usage : net-client-js détecte 401 → callback
onApiKeyInvalid→ensureApiKey({ forceRefresh: true })→ re-auth automatique. - Comportement : ignore env
MOSTA_NET_API_KEY, ignoreMOSTA_NO_AUTOREGISTER, appelleflow.refresh()au lieu deflow.run().
- Cas d'usage : net-client-js détecte 401 → callback
clearCachedApiKey({ host })helper exporté pour sign-out explicite, outils admin, tests propres. No-op tolérant si pas de cache ou@mostajs/auth-flowpas installé.
src/index.ts (modifié) : re-export clearCachedApiKey en plus d'ensureApiKey.
tests-scripts/test-auto-register.ts (modifié) : +11 nouveaux assertions (12 → 23) :
- T2.A/B/C —
MOSTA_AUTH_FLOW_URLpriorité + fallbackNET_URL+ throw si aucun - T3.A —
forceRefreshignoreMOSTA_NET_API_KEYenv - T3.B —
forceRefreshignoreMOSTA_NO_AUTOREGISTER+ cache - T4 —
clearCachedApiKey()no-op tolérant
README.md : section Release Notes v1.2.1 + usage exemple trajet 2.
docs/PLAN-V1.2.1.md (nouveau) : plan détaillé d'exécution (checklist 12 items, tests, commit format) — référence pour reprise future.
Bump
1.2.0 → 1.2.1.
Note rétro-compat
100% — apps en v1.2.0 ne sont pas affectées (les 3 ajouts sont opt-in via env var ou option de fonction).
v1.2.0 — 2026-05-02 — Auto-register apikey via @mostajs/auth-flow (Session N+4 a)
Première moitié de Session N+4 du chantier auto-register Octonet (cf. Octonet-as-Supabase/11-AUTOREGISTER-FLOW-ROADMAP.md §2.2).
Quand data-plug tourne en mode NET (MOSTA_DATA=net) et que MOSTA_NET_API_KEY n'est pas défini, on déclenche un device flow OAuth 2.0 (RFC 8628) auprès du serveur Octonet pour obtenir une apikey valide. C'est l'étape "data-plug appelle Octonet en premier" du flow end-to-end.
src/auto-register.ts (~140 lignes)
ensureApiKey({ store?, clientId?, scope?, host?, onCodeIssued? })— async- Ordre de résolution :
MOSTA_NET_API_KEYenv var → court-circuit, return tel quel- Token cached dans
~/.config/<host>/auth.json(60s skew) → return - Si
MOSTA_NO_AUTOREGISTER=true→ throw avec message explicite - Sinon → device flow auprès de
${MOSTA_NET_URL}/api/v1/auth/device/*
@mostajs/auth-flowen LAZY IMPORT (peerDep optional) — data-plug reste utilisable sans lui pour les hôtes ayant leur propre stratégie d'apikeydefaultTerminalRenderer: UX CLI propre (URL, user_code, expiration en min)- Lecture env via
@mostajs/config(cascadeMOSTA_ENV=DEV|TEST|PRODhonorée pourMOSTA_NET_URL,MOSTA_NET_API_KEY,MOSTA_HOST,MOSTA_CLIENT_ID,MOSTA_NO_AUTOREGISTER)
src/index.ts modifié
initNetDialect(): siMOSTA_NET_API_KEYabsent, appelleensureApiKey()AVANT d'instancierNetClient. Pattern lazy-import (./auto-register.js) pour ne pas tirer le code si l'host fournit l'apikey explicitement.- Re-export
ensureApiKey+EnsureApiKeyOptionsau bottom du barrel pour les hôtes qui veulent déclencher manuellement (script CLI bootstrap).
Dépendances
@mostajs/auth-flowajouté enpeerDependencyoptional +devDependency^0.1.0-alpha.3(pour les types tsc, pas requis runtime sans device flow).
Tests
12 nouveaux assertions (tests-scripts/test-auto-register.ts) — pattern @mostajs/config :
- Cascade
MOSTA_ENV=TEST→TEST_MOSTA_NET_API_KEYpriorité sur plain MOSTA_NET_API_KEYexplicite → court-circuit- Cached token valide via store custom → return direct (URL réelle
https://octonet.amia.fr/) MOSTA_NO_AUTOREGISTER=true+ pas de cache → throw expliciteMOSTA_NO_AUTOREGISTER=true+ cached → return cached- Sans
MOSTA_NET_URL→ throw clair MOSTA_CLIENT_IDoverride via env
URL réelle utilisée dans les tests : https://octonet.amia.fr/ — c'est la cible qu'on attaquera dans les sessions N+4(b/c) côté octonet-cloud routes + /device.
Bump 1.1.0 → 1.2.0.
Reste pour clore Session N+4
- N+4(b) :
octonet-cloudroutes/api/v1/auth/device/{authorize,token,approve} - N+4(c) :
octonet-cloud/devicepage (Server Component branding + consent)
Trajet d'un disconnect()
mosta-net/server.ts → data-plug.disconnect()
data-plug/index.ts:139 → orm.disconnectDialect()
mosta-orm/core/factory.ts:176 → currentDialect.disconnect()
postgres.dialect.ts:92 → pool.end()
this.pool = null
factory.ts:179-180 → currentDialect = null
currentConfig = null
index.ts:153 → setGlobalDialect(null) ← cache
data-plug vidé
Trajet d'un getDialect() immédiatement après
data-plug.getDialect() index.ts:99 cached = getGlobalDialect() = null index.ts:106 initOrmDialect() index.ts:163 orm.getDialect() factory.ts:134 currentDialect = null factory.ts:138 cfg = getConfigFromEnv() ← relit DB_DIALECT, SGBD_URI factory.ts:143 currentDialect = createDialect() ← nouvelle instance pg factory.ts:144 await connect(cfg) ← nouveau Pool ouvert factory.ts:148 initSchema(getAllSchemas()) index.ts:109 setGlobalDialect(dialect) ← ré-injecté dans le cache global
Conclusion clé : la chaîne de reconnexion est fonctionnellement saine
Usage exemple
// Variables d'environnement (cascade MOSTA_ENV honorée)
// MOSTA_DATA=net
// MOSTA_NET_URL=https://octonet.amia.fr/
// (pas de MOSTA_NET_API_KEY → device flow déclenché au premier appel)
import { getDialect } from '@mostajs/data-plug'
// Au boot, si pas d'apikey en env :
// 1. ~/.config/octonet/auth.json lu — si valide → utilisée
// 2. Sinon → terminal affiche URL + user_code, l'user approuve dans browser,
// apikey émise + persistée
// 3. NetClient initialisé avec l'apikey, calls DB possibles
const dialect = await getDialect()
const users = await dialect.find(UserSchema, {})Pour bypass en CI ou scripts non-interactifs :
export MOSTA_NET_API_KEY=ok_existing_apikey
export MOSTA_NO_AUTOREGISTER=trueLicence
AGPL-3.0-or-later — Dr Hamid MADANI [email protected]
