@mastra/core
Version:
257 lines • 11.7 kB
TypeScript
/**
* FactoryStorage — a pluggable application-storage backend contract.
*
* One `FactoryStorage` instance powers both sides of an application
* deployment's persistence:
*
* - **Agent state** (threads, messages, memory, observational memory) via
* {@link FactoryStorage.getMastraStorage}, which callers feed to the
* Mastra instance and all agent-related wiring.
* - **App tables** (application-owned collections: settings, audit trails,
* work items, integration state, ...) via the generic
* {@link FactoryStorageOps} query surface plus declarative
* {@link CollectionSchema} DDL mapping.
*
* App-table domains are written once against `ops`; backends implement the
* small query surface once (M + N, not M × N). Nothing outside a backend
* implementation may branch on the database dialect — optional capabilities
* such as {@link FactoryStorage.authDatabase} are feature-gated on presence.
*
* Contract discipline: the ops surface is deliberately small — equality-filter
* CRUD, conflict-key upsert, ordered/limit/keyset-cursor lists, and atomic
* read-modify-write. Anything not expressible here is a deliberate, reviewed
* contract extension — never raw SQL from a domain.
*
* Store packages (`@mastra/pg`, `@mastra/libsql`) ship implementations next
* to their `MastraCompositeStore` adapters, sharing one connection between
* agent state and app tables.
*/
import type { MastraCompositeStore } from './base.js';
import { StorageDomain } from './domains/base.js';
/** Values storable in (and filterable on) a collection column. */
export type CollectionValue = string | number | boolean | Date | null;
/**
* Row filter: column → required value. Multiple entries AND together.
* - A {@link CollectionValue} matches by equality; `null` matches SQL `IS NULL`.
* - `{ in: [...] }` matches any of the listed values (SQL `IN`).
* - `{}` matches every row.
*
* Column names must be declared in the collection's schema — backends reject
* unknown collections/columns instead of interpolating them.
*/
export type CollectionWhere = Record<string, CollectionValue | {
in: CollectionValue[];
}>;
/**
* Keyset cursor for stable pagination: the `orderBy` column values of the
* last row of the previous page, in the same order as `orderBy`. The next
* page contains rows strictly after that position in the sort order.
*/
export interface CollectionCursor {
values: CollectionValue[];
}
export interface CollectionListOptions {
/** Sort order; required when `cursor` is set. */
orderBy?: [column: string, dir: 'asc' | 'desc'][];
limit?: number;
/** Keyset cursor over the `orderBy` columns (see {@link CollectionCursor}). */
cursor?: CollectionCursor;
}
/**
* Closed column-type union, mapped to backend-native types.
*
* `uuid-pk` declares the collection's generated primary key: the ops layer
* assigns a UUID client-side on insert when the caller doesn't provide one,
* so every backend produces identical rows. A collection may instead mark one
* caller-supplied column with `primaryKey: true` (natural keys, e.g. a
* session id).
*
* Value normalization is part of the contract regardless of dialect:
* `timestamp` columns round-trip as `Date`, `json` as parsed values,
* `boolean` as booleans, and `bigint` as JS numbers (safe integers — e.g.
* GitHub ids fit well inside 2^53).
*/
export type CollectionColumnType = 'text' | 'bigint' | 'integer' | 'boolean' | 'json' | 'timestamp' | 'uuid-pk';
export interface CollectionColumnSpec {
type: CollectionColumnType;
/** Columns are NOT NULL unless marked nullable. */
nullable?: boolean;
/**
* Natural primary key (caller-supplied on insert). Mutually exclusive with
* a `uuid-pk` column; exactly one primary key per collection.
*/
primaryKey?: boolean;
/**
* DDL-level default literal. Required when additively introducing a
* NOT NULL column to a collection that may already have rows (e.g.
* `actor_type text NOT NULL DEFAULT 'human'`).
*/
default?: string | number | boolean;
}
/**
* Unique index. The optional partial forms cover the two shapes app schemas
* need: `whereNotNull` (unique per non-null natural key) and `whereNull`
* (unique per scope where an owner column is absent).
*/
export interface CollectionUniqueIndexSpec {
name: string;
columns: string[];
/** Index only rows where this column IS NOT NULL. */
whereNotNull?: string;
/** Index only rows where this column IS NULL. */
whereNull?: string;
}
export interface CollectionIndexSpec {
name: string;
columns: string[];
}
/**
* Declarative collection definition, mapped to backend DDL by
* {@link FactoryStorage.ensureCollections}. Evolution is additive only:
* re-running with new columns/indexes adds them; nothing is dropped or
* retyped.
*/
export interface CollectionSchema {
name: string;
/** Column name → spec. Rows returned by ops are keyed by these names. */
columns: Record<string, CollectionColumnSpec>;
uniqueIndexes?: CollectionUniqueIndexSpec[];
indexes?: CollectionIndexSpec[];
}
/**
* Tagged database handle for auth libraries (e.g. better-auth). Consumers
* narrow on `dialect` to build their driver adapter — a supported contract,
* unlike sniffing store internals. `custom` passes an adapter/instance the
* auth library accepts as-is.
*/
export type FactoryAuthDatabase = {
dialect: 'postgres';
pool: unknown;
} | {
dialect: 'libsql';
client: unknown;
} | {
dialect: 'custom';
database: unknown;
};
/**
* Thrown by `insertOne`/`upsertOne` when a unique constraint rejects the row.
* Backends map their native duplicate-key errors onto this type so domains
* can implement insert-or-recover races portably.
*/
export declare class UniqueViolationError extends Error {
readonly collection: string;
constructor(collection: string, options?: {
cause?: unknown;
});
}
/**
* The generic query surface app-table domains are written against.
*
* Rows (`T`) are plain objects keyed by schema column names; domains own any
* mapping to their public camelCase shapes. All methods throw if the
* collection (or any referenced column) was not registered via
* `ensureCollections`.
*/
export interface FactoryStorageOps {
findOne<T extends Record<string, unknown>>(collection: string, where: CollectionWhere): Promise<T | null>;
findMany<T extends Record<string, unknown>>(collection: string, where: CollectionWhere, opts?: CollectionListOptions): Promise<T[]>;
/**
* Insert one row, returning it (with the generated `uuid-pk` populated).
* Throws {@link UniqueViolationError} on any unique-constraint conflict.
*/
insertOne<T extends Record<string, unknown>>(collection: string, row: Partial<T>): Promise<T>;
/**
* Insert, or update the existing row that matches `conflictKeys` (which
* must be covered by a unique index). Non-key columns present in `row`
* replace the stored values; the existing primary key is preserved.
*/
upsertOne<T extends Record<string, unknown>>(collection: string, conflictKeys: string[], row: Partial<T>): Promise<T>;
/** Set columns on every matching row. Returns the number of rows updated. */
updateMany(collection: string, where: CollectionWhere, set: Record<string, unknown>): Promise<number>;
/** Delete every matching row. Returns the number of rows deleted. */
deleteMany(collection: string, where: CollectionWhere): Promise<number>;
/**
* Atomic read-modify-write of one matching row. `fn` receives the current
* row and returns the columns to set — or `null` to abort without writing
* (the unmodified row is returned; use a closure flag to distinguish abort
* from success). Returns `null` when no row matches.
*
* Isolation: pg runs `fn` inside a `SELECT ... FOR UPDATE` transaction;
* libsql serializes through its single-writer path. Either way, concurrent
* `updateAtomic` calls on the same row never lose each other's writes.
*/
updateAtomic<T extends Record<string, unknown>>(collection: string, where: CollectionWhere, fn: (row: T) => Partial<T> | null | Promise<Partial<T> | null>): Promise<T | null>;
}
/**
* Base class for application domains owned by a {@link FactoryStorage}.
* Domains are bound once when registered and share their owner's connection.
*/
export declare abstract class FactoryStorageDomain extends StorageDomain {
#private;
readonly name: string;
protected constructor(name: string);
/** @internal Bound by {@link FactoryStorage.registerDomain}. */
__bindFactoryStorage(storage: FactoryStorage): void;
protected get storage(): FactoryStorage;
/**
* Initialize this domain (via its owning storage) if it hasn't been yet.
* Lets consumers holding a domain handle run the same fail-soft readiness
* check as {@link FactoryStorage.ensureDomainReady} without also needing a
* reference to the storage backend.
*/
ensureReady(): Promise<void>;
protected get ops(): FactoryStorageOps;
protected ensureCollections(schemas: CollectionSchema[]): Promise<void>;
}
/**
* A pluggable application-storage backend: one database powering agent state
* (via {@link getMastraStorage}) and app-owned collections (via {@link ops}).
*/
export declare abstract class FactoryStorage {
#private;
/**
* Agent-state store (threads, messages, memory, OM) for this database,
* sharing this backend's connection. Callers pass the result to the Mastra
* instance and all agent-related wiring. Lazily constructed; returns the
* same instance on repeat calls.
*/
abstract getMastraStorage(): MastraCompositeStore;
/** Open/validate the backend, then initialize registered domains fail-soft. */
init(): Promise<void>;
/** Backend-specific connection initialization. */
protected abstract initStorage(): Promise<void>;
registerDomain<T extends FactoryStorageDomain>(domain: T): T;
getDomain<T extends FactoryStorageDomain = FactoryStorageDomain>(name: string): T;
hasDomain(name: string): boolean;
domainNames(): string[];
isDomainReady(name: string): boolean;
domainInitError(name: string): unknown;
ensureDomainReady(name: string): Promise<void>;
/**
* Map each domain's declarative schema to backend DDL. Idempotent and
* additive: safe to re-run, never drops or retypes anything. Registers the
* schemas so `ops` can validate identifiers and normalize values.
*/
abstract ensureCollections(schemas: CollectionSchema[]): Promise<void>;
/** The generic query surface domains are written against. */
abstract readonly ops: FactoryStorageOps;
/**
* Run a group of app-table operations atomically. The callback receives an
* ops instance bound to the transaction; callers must not use `this.ops`
* inside it. Serializable callbacks may be retried after a serialization
* failure and therefore must contain database operations only.
*/
abstract withTransaction<T>(fn: (ops: FactoryStorageOps) => Promise<T>, options?: {
isolationLevel?: 'serializable';
}): Promise<T>;
/** Release the backend's connections (tests, shutdown). */
abstract close(): Promise<void>;
/**
* A tagged database handle auth libraries can consume (see
* {@link FactoryAuthDatabase}). Absent → auth integrations require a
* user-provided instance.
*/
authDatabase?(): FactoryAuthDatabase;
}
//# sourceMappingURL=factory-storage.d.ts.map