@nzelajs/persistence-prisma
v0.1.0
Published
Prisma/Postgres persistence adapter for the Nzela engine: tenant-scoped unit of work with PostgreSQL row-level security, optimistic-locked instances, append-only event log, versioned immutable blueprint installs, approval and org-chart stores. Narrow Pris
Downloads
51
Maintainers
Readme
@nzelajs/persistence-prisma
English below - Version francaise plus bas.
The Prisma/Postgres persistence adapter for the Nzela engine: the single adapter that knows the
database. It binds PostgreSQL row-level security per transaction, optimistic-locks instances, keeps
an append-only event log, stores versioned immutable blueprint installs and the approval/org-chart
companion stores. It depends on a NARROW PrismaLike surface: no client is generated here and
nothing is imported from @prisma/client at runtime. Vendor-free by the Nzela doctrine.
English
Install
npm add @nzelajs/persistence-prisma @nzelajs/ports
# and, in your app, the real client generated from your schema:
npm add prisma @prisma/clientprisma and @prisma/client are OPTIONAL peer dependencies: this package never imports them. You
generate your own client from a schema shaped like prisma/schema.reference.prisma, and it is
structurally assignable to PrismaLike.
What it provides
PrismaUnitOfWorkimplementsUnitOfWork.withTenant(tenantId, fn)opens an interactive Prisma transaction and, as its FIRST statement, binds the tenant context withSELECT set_config('app.tenant_id', $1, true)so every subsequent query is tenant-isolated at the database level. It handsfnaRepositoryRegistryscoped to that transaction. Fail-closed: an empty tenant id is refused before any connection is used.- All repositories of the registry, on the transaction:
WorkflowInstanceRepo: optimistic locking via a conditionalupdateManyon{ id, version }(a zero count raisesConcurrencyConflictError); every write bumpsversion.EventLog: append +listByInstanceincreatedAtorder (append-only history).TimerStore,ExternalWaitStore,DependencyStore.BlueprintInstallStore:publishvalidates the graph withparseBlueprintBEFORE writing (fail-closed), computes the next version, archives the previous ACTIVE and inserts the new ACTIVE, all inside the same transaction (atomic). In-flight instances stay pinned viainstallId.OutboxRepository: transactional outbox append.ApprovalTaskStore,DeskRuleStore(decreasing-specificity resolution),TenantSettingsStore,OrgStore,DirectoryStore,AuthzStore(loadGrantsForUser, fail-closed provenance).
- A NARROW
PrismaLike/PrismaTxsurface (prisma-like.ts) plus fail-closed row mappers (mapping.ts): union columns stayStringin the database and are narrowed to the domain literals here; an unreadable value is dropped or defaulted, never widened. prisma/schema.reference.prisma: a generic Postgres reference schema (every model carriestenantIdplus indexes) with a documented RLS policy. Not generated by this package.
Row-level security (the convention this adapter requires)
Security is enforced by the database, not by application filters. The host materializes, once (a raw SQL migration, not Prisma-managed):
-- Reads the transaction-local tenant, fail-closed (NULL when unset).
CREATE OR REPLACE FUNCTION current_tenant_id() RETURNS text
LANGUAGE sql STABLE AS $$ SELECT current_setting('app.tenant_id', true) $$;
-- For every table (example on "WorkflowInstance"): RESTRICTIVE + FORCE, gating reads and writes.
ALTER TABLE "WorkflowInstance" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "WorkflowInstance" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "WorkflowInstance"
AS RESTRICTIVE
USING ("tenantId" = current_tenant_id())
WITH CHECK ("tenantId" = current_tenant_id());PrismaUnitOfWork sets app.tenant_id (transaction-local, is_local = true) before any query, so a
transaction can neither read nor write another tenant's rows. When the setting is unset,
current_tenant_id() is NULL and the predicate is false: the transaction sees nothing (fail-closed).
RLS self-probe (ON by default). On the FIRST withTenant, the unit of work reads
current_tenant_id() back and asserts it equals the bound tenant. A host that deployed the adapter
but forgot to install prisma/rls.sql gets a clear error at boot instead of silently relying on the
belt-and-suspenders tenantId filters. Opt out with new PrismaUnitOfWork(prisma, {
verifyRlsOnFirstUse: false }) only when the probe cannot run (e.g. a fake in tests).
Example
import { PrismaClient } from "@prisma/client";
import { PrismaUnitOfWork } from "@nzelajs/persistence-prisma";
const prisma = new PrismaClient();
const uow = new PrismaUnitOfWork(prisma); // structurally a PrismaLike
await uow.withTenant("acme", async (repos) => {
const c = await repos.instances.create({
requestTypeId: "leave",
status: "DRAFT",
data: {},
});
await repos.instances.updateStatus(c.id, {
fromVersion: 1,
toStatus: "GATE",
});
});Francais
Installation
npm add @nzelajs/persistence-prisma @nzelajs/ports
# et, cote application, le vrai client genere depuis votre schema :
npm add prisma @prisma/clientprisma et @prisma/client sont des peer dependencies OPTIONNELLES : ce paquet ne les importe
jamais. Vous generez votre propre client depuis un schema de la forme de
prisma/schema.reference.prisma ; il est structurellement assignable a PrismaLike.
Ce que le paquet fournit
PrismaUnitOfWorkimplementeUnitOfWork.withTenant(tenantId, fn)ouvre une transaction Prisma interactive et, en TOUT PREMIER, lie le contexte tenant viaSELECT set_config('app.tenant_id', $1, true): chaque requete suivante est isolee par tenant au niveau base. Il passe afnunRepositoryRegistrylimite a cette transaction. Fail-closed : un tenant vide est refuse avant toute connexion.- Tous les depots du registre, sur la transaction : verrou optimiste par version
(
ConcurrencyConflictErrorsi conflit), journal d'evenements append-only ordonne, installs de blueprint versionnes/immuables/epingles (validationparseBlueprintAVANT ecriture, archivage atomique de l'ACTIVE precedente), outbox transactionnel, stores d'approbation et d'organigramme. - Surface NARROW
PrismaLike/PrismaTx+ mappeurs fail-closed : les colonnes d'union restentStringen base et sont retrecies vers les litteraux du domaine ici ; une valeur illisible est ecartee ou ramenee a un defaut sur, jamais elargie. prisma/schema.reference.prisma: schema Postgres de reference (colonnetenantIdpartout + index) avec la politique RLS documentee. Non genere par ce paquet.
Securite au niveau ligne (la convention exigee)
La securite est portee par la base, pas par des filtres applicatifs. L'hote materialise une fois (une
migration SQL brute, hors Prisma) la fonction current_tenant_id() (lecture fail-closed de
app.tenant_id) et, pour chaque table, une politique RESTRICTIVE + FORCE portant sur USING et
WITH CHECK (voir le bloc SQL de la section anglaise et prisma/schema.reference.prisma).
PrismaUnitOfWork pose app.tenant_id (portee transaction, is_local = true) avant toute requete :
la transaction ne peut ni lire ni ecrire les lignes d'un autre tenant ; contexte absent = rien de
visible (fail-closed).
Sonde RLS (ACTIVEE par defaut). Au PREMIER withTenant, l'unite de travail relit
current_tenant_id() et verifie qu'il vaut le tenant lie. Un hote qui a deploye l'adaptateur sans
installer prisma/rls.sql obtient une erreur claire au demarrage au lieu de dependre en silence des
filtres tenantId de ceinture. Desactivable via new PrismaUnitOfWork(prisma, {
verifyRlsOnFirstUse: false }) uniquement quand la sonde ne peut pas s'executer (ex. un fake de test).
License
Apache-2.0. See LICENSE and NOTICE.
