Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

Extensions

Extend request, query, statement, observation, client, and model boundaries

$extends() returns an immutable derived client. The base client is unchanged, and the derived client carries one ordered extension chain and its own operation scope.

import { defineExtension } from "viborm";

const timing = defineExtension({
  name: "timing",
  query: async ({ proceed }) => {
    const startedAt = performance.now();
    const value = await proceed();
    recordDuration(performance.now() - startedAt);
    return value;
  },
});

const measured = client.$extends(timing);

The six capabilities

An extension has one name and any subset of exactly six capabilities:

Capability Boundary Authority
request Unvalidated model-operation input Return a synchronous patch before core validation
query Prepared logical model or raw operation Wrap execution; a read can return without provider execution
statement One materialized typed Sql statement Return a replacement Sql before rendering
observe A real lifecycle unit Read completion only; cannot change application behavior
client Derived client proxy Add typed dollar-prefixed methods
model Derived model delegate Add typed model methods

These are one extension language, not one universal hook. Request transforms are synchronous. Query interceptors are asynchronous and authoritative. Statement transforms are trusted low-level SQL rewrites. Observers are protected and failure-contained. Client and model factories change the static surface.

Official extensions

Official extensions use the same composition language while retaining private access to the core facts they need:

Operation lifecycle

Calling a model method still creates a lazy operation. Its operation handlers start only when that operation is awaited or admitted to an array transaction. Definition normalization and client/model method factories instead run when a derived root or transaction view is constructed:

operation observer begins
  request transforms
  default omit
  core validation and mandatory preparation
  query interceptors
    official cache around the prepared core read
      planning, compilation, and late validation
      for each physical statement
        statement observer begins
        statement transform
        render and provider execution
      core result parse
    query post-work
operation observer completes

Array transactions add one outer batch unit. Every member completes request preparation and query admission before provider work begins. Fallback execution then runs members in order inside the real transaction or savepoint; a native batch uses one provider submission while retaining one logical operation and one physical statement unit for each member statement.

The statement transform receives safe model statements and tagged/fragment raw Sql. It excludes verbatim $queryRawUnsafe and $executeRawUnsafe text. Physical statement observation still covers unsafe raw without disclosing SQL or parameters.

Query continuation authority

For mutations and raw operations, a matched query interceptor must call proceed() exactly once. Reads may deliberately return without it. Once proceed() starts, the child result or error is authoritative: returning a fabricated value does not replace it. A rejected child and rejected post-work remain distinct failures.

In $transaction([...]), all matched query chains must reach proceed() before any provider work. A refusal, double call, late detached call, or rejected handler aborts admission and submits nothing.

Protected observation

An ordinary observer receives only a frozen lifecycle unit and a protected completion:

const audit = defineExtension({
  name: "audit",
  observe(unit, proceed) {
    void proceed().then((completion) => {
      record({ unit, completion });
    });
  },
});

Units are discriminated as operation, statement, batch, transaction, savepoint, segment, connection, or cache. Public facts are limited to the unit kind, applicable model/operation spelling, duration, success/failure, a sanitized error summary, and optional commit certainty. Observers never see the application result, rows, SQL, parameters, cache keys, raw provider error, driver, correlation identity, or internal tokens. A throw, rejection, or never-settling promise returned by an ordinary observer cannot delay or alter the application.

The official instrumentation() extension uses the same protected rail plus private core facts. Public observers do not gain those facts or their disclosure authority.

Reusable and schema-bound definitions

defineExtension({...}) creates a schema-generic extension. Its request and query contributions use the polymorphic all-operation function form. Schema-specific model/operation maps use the curried binder:

const tenant = defineExtension<typeof schema>()({
  name: "tenant",
  request: {
    post: {
      findMany({ input }) {
        return { where: addTenant(input.where, tenantId) };
      },
    },
  },
});

Authorization boundary

Request and query capabilities are useful policy foundations, but a root filter is not complete RBAC. Nested reads and writes, relation membership, field use, raw SQL, statement transforms, and data-dependent write scopes need one graph-wide semantic policy owner. VibORM does not currently export an RBAC helper. omit is presentation control, not authorization: query-level and client-default omission can be overridden, and even model .omit() does not replace database authorization.

Was this page helpful?