persistence-js : le futur socle de persistence des applications Exygen
Chaque application Exygen a besoin, à un moment ou un autre, de la même chose : décrire un modèle de données, l'enregistrer en base, le faire évoluer sans casser la production, et le requêter proprement depuis le code. Jusqu'ici, chaque application réinventait sa propre couche d'accès aux données. persistence-js met fin à ça.
Le problème
En TypeScript/Node, on a souvent le choix entre deux mauvaises options : une couche SQL brute (rapide, mais où l'on réécrit à la main chaque INSERT/UPDATE et où l'on recalcule les relations partout), ou un ORM façon ActiveRecord (entités qui héritent d'une classe de base, suivi des modifications implicite, changement de moteur de base de données quasiment impossible en pratique).
persistence-js vise le juste milieu : un moteur de persistence façon JDO/JPA, où le modèle de données est déclaré une seule fois, en JSON — et où tout le reste (types TypeScript, requêtes typées, schéma SQL, index de recherche) en découle.
Le modèle, comme donnée
On décrit ses entités une seule fois, sous forme de JSON :
1// model.json
2{
3 "kind": "MODEL", "schema": "blog",
4 "entities": [
5 {
6 "kind": "ENTITY", "id": "e-author", "name": "Author",
7 "fields": [
8 {"kind": "FIELD", "id": "f-a-id", "name": "id", "type": "INTEGER", "primaryKey": true, "autoIncrement": true},
9 {"kind": "FIELD", "id": "f-a-firstname", "name": "firstName", "type": "STRING", "allowNull": false},
10 {"kind": "FIELD", "id": "f-a-email", "name": "email", "type": "STRING", "unique": true}
11 ],
12 "relations": [
13 {"kind": "RELATION", "id": "r-author-posts", "name": "posts", "source": "Author", "target": "Post",
14 "type": "Composition", "cardinality": "Many", "foreignKey": "author_id"}
15 ]
16 }
17 ]
18}
À partir de ce fichier, les interfaces TypeScript et les accesseurs de requête typés sont générés, jamais écrits à la main :
1// généré : src/generated/entities/Author.ts
2export interface Author extends PersistenceCapable {
3 firstName: string;
4 email: string;
5 posts: Post[];
6}
7export function makeAuthor(id: number, firstName: string, email: string): Author { /* ... */ }
Du CRUD sans une ligne de SQL
1import {PersistenceManagerFactoryImpl} from '@exygen/persistence-js';
2import {makeAuthor} from './generated/entities';
3
4const factory = await PersistenceManagerFactoryImpl.create({
5 driver: {type: 'postgresql', host: 'localhost', database: 'blog', user: 'user', password: 'password'},
6 models: './model.json',
7 schemaInstall: 'align', // crée/aligne le schéma sur le modèle
8});
9
10const manager = await factory.createManager();
11
12const tx = manager.beginTransaction();
13const author = await manager.create(makeAuthor(0, 'Alice', 'alice@example.com'));
14await tx.commit(); // un seul batch SQL, ordonné, atomique
15
16author.email = 'alice@exygen.fr'; // simple mutation d'un objet plain JS...
17await manager.update(author); // ...le suivi des changements fait le reste
18
19await manager.close();
20await factory.closeAll();
Pas de .save(), pas de classe de base à hériter, pas de diff manuel avant/après : muter un champ sur un objet chargé suffit, le moteur sait qu'il faudra un UPDATE au prochain commit().
Des requêtes typées, pas des chaînes de caractères
1const posts = await manager.find('Post', {
2 where: p => and(p.active.isTrue(), p.title.like('Hello%')),
3 sortAsc: p => p.title,
4 take: 10,
5});
L'autocomplétion porte sur les champs et relations réels du modèle — plus de nom de colonne mal orthographié découvert en production.
Pourquoi ça devient notre socle commun
Plusieurs raisons ont motivé ce choix pour l'ensemble des back-ends Exygen :
- Un seul modèle, plusieurs consommateurs — le même
MetaModelJSON pilote les types TypeScript, les requêtes, le schéma SQL et, en option, l'indexation de recherche. Un champ changé une fois, tout le reste suit. - Écritures prévisibles — suivi automatique des modifications +
commit()explicite : ce qui sera écrit, et quand, ne dépend jamais d'un mécanisme caché. - Évolution de schéma sans risque — mode
update(additif, sûr en production) oualign(strict, pour le dev/CI), avec diff et dry-run. - Pas d'enfermement chez un éditeur de base de données — PostgreSQL, MariaDB ou MySQL : changer de moteur est une configuration, pas une réécriture.
- On ne paie que ce qu'on utilise — cache, recherche plein texte, stockage de fichiers, plugin Express : chaque brique est un package
@exygen/*séparé, installé uniquement si besoin. - Multi-tenant par construction — les "layers" (tenant, suppression logique, exercice fiscal...) se déclarent sur le modèle une fois, et s'appliquent automatiquement à chaque requête, sans les réécrire application par application.
En unifiant la couche de persistence sur une seule bibliothèque partagée, chaque nouvelle application Exygen démarre avec un accès aux données fiable, testé et déjà documenté — au lieu de repartir de zéro à chaque projet.
Pour aller plus loin
1npm install @exygen/persistence-js
2npm install pg # ou mariadb, selon le moteur cible
persistence-js et ses modules (jmetadata, jdriver, jcache, tslogger, search-index, file-storage, express, query-assist) sont publiés indépendamment sous @exygen/* — on n'installe que ce dont on a besoin.