persistence-js: the future persistence layer of Exygen applications
Every Exygen application needs, sooner or later, the same thing: describe a data model, persist it to a database, evolve it without breaking production, and query it cleanly from code. Until now, each application reinvented its own data access layer. persistence-js puts an end to that.
The problem
In TypeScript/Node, you often have to choose between two bad options: a raw SQL layer (fast, but where you hand-write every INSERT/UPDATE and recompute relations everywhere), or an ActiveRecord-style ORM (entities that inherit from a base class, implicit change tracking, and switching database engines that is nearly impossible in practice).
persistence-js aims for the middle ground: a JDO/JPA-style persistence engine, where the data model is declared once, in JSON — and everything else (TypeScript types, typed queries, SQL schema, search index) follows from it.
The model, as data
You describe your entities once, as 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}
From this file, TypeScript interfaces and typed query accessors are generated, never hand-written:
1// generated: 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 { /* ... */ }
CRUD without a single line of 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', // creates/aligns the schema with the model
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(); // a single ordered, atomic SQL batch
15
16author.email = 'alice@exygen.fr'; // just mutate a plain JS object...
17await manager.update(author); // ...change tracking does the rest
18
19await manager.close();
20await factory.closeAll();
No .save(), no base class to inherit from, no manual before/after diff: mutating a field on a loaded object is enough, the engine knows an UPDATE will be needed at the next commit().
Typed queries, not strings
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});
Autocompletion works on the model's actual fields and relations — no more misspelled column name discovered in production.
Why it is becoming our common foundation
Several reasons drove this choice for all Exygen back-ends:
- One model, several consumers — the same JSON
MetaModeldrives TypeScript types, queries, the SQL schema and, optionally, search indexing. Change a field once, everything else follows. - Predictable writes — automatic change tracking + explicit
commit(): what will be written, and when, never depends on a hidden mechanism. - Safe schema evolution —
updatemode (additive, safe in production) oralignmode (strict, for dev/CI), with diff and dry-run. - No database vendor lock-in — PostgreSQL, MariaDB, or MySQL: switching engines is a configuration change, not a rewrite.
- Pay only for what you use — cache, full-text search, file storage, Express plugin: each building block is a separate
@exygen/*package, installed only when needed. - Multi-tenant by design — "layers" (tenant, soft delete, fiscal year...) are declared on the model once, and apply automatically to every query, without rewriting them application by application.
By unifying the persistence layer on a single shared library, every new Exygen application starts with reliable, tested, already documented data access — instead of starting from scratch on every project.
Going further
1npm install @exygen/persistence-js
2npm install pg # or mariadb, depending on the target engine
persistence-js and its modules (jmetadata, jdriver, jcache, tslogger, search-index, file-storage, express, query-assist) are published independently under @exygen/* — you only install what you need.