A Prisma Next (8.x) / Postgres toolkit for Node.js & TypeScript back-ends — the persistence helpers behind @imqueue framework services. It bundles a set of Prisma Next query middlewares (soft-delete, authorship stamping, audit trail, row-level access scope) that rewrite the statement before it is lowered to SQL, plus Postgres operational helpers (row archiving, change-notify triggers, SQL log formatting).
Their per-model configuration is derived from the emitted contract.json
rather than generated: Prisma Next has no custom-generator protocol and needs
none, since the contract already names every model, field and physical column.
Documentation: full guides, tutorial and API reference at imqueue.org. Commercial licensing & support for closed-source products at imqueue.com.
Using an AI assistant? Point it at imqueue.org/llms.txt for a machine-readable index of the docs, or see AGENTS.md. Current version, licence and Node floor for every package: imqueue.org/status.json.
Related packages:
- @imqueue/core - Fast JSON message queue over Redis for inter-service communication.
- @imqueue/rpc - RPC-like client/service implementation over @imqueue/core.
- @imqueue/validation - Zod-backed decorator validation (used by the generated model classes).
- Soft delete and authorship — a
DELETEbecomes adeletedAtstamp, stamped rows disappear from reads, and every write records who made it. - Access scope — every read, update and delete is narrowed to the rows the caller may see, in the data layer rather than at each call site.
- Audit trail — every write to a nominated table recorded with the actor, the action and the row as the database returned it.
- Row archiving — aged rows moved into a mirror
archiveschema on a pg_cron schedule. - Change-notify triggers — Postgres
NOTIFYon every row change.
Filtering applies across the whole statement, not just its outermost
FROM. Prisma Next compiles a relation read into one statement holding several
selects, so a filter on the root alone would return soft-deleted and
out-of-scope rows through any include.
- Node.js >= 22.12
prisma8.x and@prisma/orm-postgres(peer dependency)- PostgreSQL 15 or newer
npm i @imqueue/pg-prismaimport { dataLayer } from '@imqueue/pg-prisma';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './prisma/contract.d.ts';
import contractJson from './prisma/contract.json' with { type: 'json' };
const layer = dataLayer({
contract: contractJson,
scope: { Portfolio: { portfolio: ['id'] } },
resolvers: { portfolio: () => currentPortfolioIds() },
getActorId: currentActorId,
audit: {
connectionString: process.env.DATABASE_URL!,
config: { table: 'AuditLog', columns: { /* ... */ } },
getPrincipal: currentPrincipal,
},
});
export const db = postgres<Contract>({
contractJson,
url: process.env.DATABASE_URL!,
middleware: layer.middleware,
});dataLayer returns the middlewares already composed. That is the point: a
caller never orders them, and so cannot order them wrongly. Call
layer.close() on shutdown to release the audit pool.
Scope is the one thing that cannot be derived from the contract — Prisma Next
has no schema-level annotation to carry it — so it is declared where
dataLayer is called, keyed by model and field:
scope: {
Portfolio: { portfolio: ['id'] },
User: { user: ['createdBy', 'id'] },
}Columns within one level are OR-ed; levels are AND-ed together. A resolver
returning undefined leaves its level inactive, a value or array restricts,
and null or an empty array denies everything. Get the composition backwards
and the failure is a data leak rather than an error, so a scope naming a
model the contract does not define is a throw, not a silent no-op.
Prisma Next emits contract.d.ts, which carries the types but not the
decorated classes. @classType/@property are what the
@imqueue/rpc client generator reads, and an
undecorated type is dropped from the generated client with no error — so the
DTO classes are emitted here, from the same contract:
import { emitModels, parseImportMap } from '@imqueue/pg-prisma';
await writeFile('src/generated/models.ts', emitModels({ contract }));By default the emitted file imports @imqueue/rpc directly. Pass imports to
point it somewhere else:
emitModels({
contract,
imports: parseImportMap('@imqueue/rpc=@my-org/runtime'),
});// before
import { classType, property } from '@imqueue/rpc';
// after
import { classType, property } from '@my-org/runtime';Why this exists. The decorators are only meaningful to the registry that
defined them, so @imqueue/rpc, @imqueue/validation and zod each have to
be a single copy shared with the service. A second copy fails silently
rather than loudly — a second decorator registry nothing reads, or a ZodError
that fails instanceof. The reliable way to guarantee one copy is for one
package to own the dependency and re-export it, with every service taking it
from there; redirecting the emitted imports is what makes that possible.
Redirecting several runtimes at one package merges them into a single statement, rather than emitting the same specifier three times:
parseImportMap(
'zod=@base, @imqueue/rpc=@base, @imqueue/validation=@base',
);
// import { classType, property, validatable, validate, z } from '@base';Redirecting a module the generator never emits throws rather than being ignored, because the alternative is believing a redirection was applied while the generated files still point at the original.
stamp, accessScope and audit are exported individually for cases
dataLayer does not cover, and deriveDataLayer produces the config they take.
The middlewares commute — stamp merges what were two order-dependent Prisma 7
extensions — so there is no required order between them.
Tests run on the native Node.js test runner (node:test) with node:assert and
no external test framework:
git clone git@github.com:imqueue/pg-prisma.git
cd pg-prisma
npm install
npm testTo produce a coverage report use:
npm run test-coverage # prints coverage summary to the console
npm run test-lcov # writes coverage/lcov.infoThis project is licensed under the GNU General Public License v3.0. See the LICENSE