@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
844 lines (839 loc) • 30.6 kB
JavaScript
import { MastraBase } from './chunk-WENZPAHS.js';
import { execFile } from 'child_process';
import { realpathSync } from 'fs';
import { relative } from 'path';
// src/storage/base.ts
var EDITOR_DOMAINS = [
"agents",
"promptBlocks",
"scorerDefinitions",
"mcpClients",
"mcpServers",
"workspaces",
"skills",
"favorites"
];
function normalizePerPage(perPageInput, defaultValue) {
if (perPageInput === false) {
return Number.MAX_SAFE_INTEGER;
} else if (perPageInput === 0) {
return 0;
} else if (typeof perPageInput === "number" && perPageInput > 0) {
return perPageInput;
} else if (typeof perPageInput === "number" && perPageInput < 0) {
throw new Error("perPage must be >= 0");
}
return defaultValue;
}
function calculatePagination(page, perPageInput, normalizedPerPage) {
return {
offset: perPageInput === false ? 0 : page * normalizedPerPage,
perPage: perPageInput === false ? false : normalizedPerPage
};
}
var MastraCompositeStore = class extends MastraBase {
hasInitialized = null;
shouldCacheInit = true;
id;
stores;
/**
* When true, automatic initialization (table creation/migrations) is disabled.
*/
disableInit = false;
/**
* Retained references to the parent stores supplied via composition. `init()`
* delegates to these so the parent's own `init()` logic (pragmas, ordered
* DDL, init coalescing, etc.) runs instead of being bypassed by the
* composite iterating the inner domains in parallel — which was the cause
* of the SQLITE_BUSY / "no such table" races reported in issue #16782.
*/
parentDefault;
parentEditor;
constructor(config) {
const name = config.name ?? "MastraCompositeStore";
if (!config.id || typeof config.id !== "string" || config.id.trim() === "") {
throw new Error(`${name}: id must be provided and cannot be empty.`);
}
super({
component: "STORAGE",
name
});
this.id = config.id;
this.disableInit = config.disableInit ?? false;
if (config.default || config.editor || config.domains) {
const defaultStores = config.default?.stores;
const editorStores = config.editor?.stores;
const domainOverrides = config.domains ?? {};
this.parentDefault = config.default;
this.parentEditor = config.editor;
const hasDefaultDomains = defaultStores && Object.values(defaultStores).some((v) => v !== void 0);
const hasEditorDomains = editorStores && Object.values(editorStores).some((v) => v !== void 0);
const hasOverrideDomains = Object.values(domainOverrides).some((v) => v !== void 0);
if (!hasDefaultDomains && !hasEditorDomains && !hasOverrideDomains) {
throw new Error(
"MastraCompositeStore requires at least one storage source. Provide a default storage, an editor storage, or domain overrides."
);
}
const editorDomainSet = new Set(EDITOR_DOMAINS);
const resolve = (key) => {
if (domainOverrides[key] !== void 0) return domainOverrides[key];
if (editorDomainSet.has(key) && editorStores?.[key] !== void 0) return editorStores[key];
return defaultStores?.[key];
};
this.stores = {
memory: resolve("memory"),
workflows: resolve("workflows"),
scores: resolve("scores"),
observability: resolve("observability"),
agents: resolve("agents"),
datasets: resolve("datasets"),
experiments: resolve("experiments"),
promptBlocks: resolve("promptBlocks"),
scorerDefinitions: resolve("scorerDefinitions"),
mcpClients: resolve("mcpClients"),
mcpServers: resolve("mcpServers"),
workspaces: resolve("workspaces"),
skills: resolve("skills"),
favorites: resolve("favorites"),
blobs: resolve("blobs"),
backgroundTasks: resolve("backgroundTasks"),
schedules: resolve("schedules"),
channels: resolve("channels")
};
}
}
/**
* Get a domain-specific storage interface.
*
* @param storeName - The name of the domain to access ('memory', 'workflows', 'scores', 'observability', 'agents')
* @returns The domain storage interface, or undefined if not available
*
* @example
* ```typescript
* const memory = await storage.getStore('memory');
* if (memory) {
* await memory.saveThread({ thread });
* }
* ```
*/
async getStore(storeName) {
return this.stores?.[storeName];
}
/**
* Initialize all domain stores.
*
* When a parent store was supplied via `default` or `editor`, delegate to
* its own `init()` first. Each adapter owns its `init()` contract — it may
* apply connection-level setup, run migrations, enforce DDL ordering, or
* coalesce concurrent callers. Calling each domain's `init()` directly
* against the parent's shared client would bypass all of that and can
* corrupt or partially create schema (see issue #16782 for the SQLite
* symptom).
*
* Any remaining domains that did NOT come from a parent (e.g. supplied via
* the explicit `domains` override pointing at a different store) are then
* initialized individually — but only the ones the parents didn't already
* cover, so we never double-init the same domain instance.
*/
async init() {
if (this.shouldCacheInit && await this.hasInitialized) {
return;
}
this.hasInitialized = this.#runInit();
await this.hasInitialized;
}
async #runInit() {
const uniqueParents = /* @__PURE__ */ new Set();
if (this.parentDefault) uniqueParents.add(this.parentDefault);
if (this.parentEditor) uniqueParents.add(this.parentEditor);
await Promise.all([...uniqueParents].map((parent) => parent.init()));
const alreadyInitialized = /* @__PURE__ */ new Set();
const addParentDomains = (parent) => {
if (!parent?.stores) return;
for (const domain of Object.values(parent.stores)) {
if (domain) alreadyInitialized.add(domain);
}
};
addParentDomains(this.parentDefault);
addParentDomains(this.parentEditor);
const initTasks = [];
const maybeInit = (domain) => {
if (!domain || alreadyInitialized.has(domain)) return;
initTasks.push(domain.init());
alreadyInitialized.add(domain);
};
if (this.stores) {
maybeInit(this.stores.memory);
maybeInit(this.stores.workflows);
maybeInit(this.stores.scores);
maybeInit(this.stores.observability);
maybeInit(this.stores.agents);
maybeInit(this.stores.datasets);
maybeInit(this.stores.experiments);
maybeInit(this.stores.promptBlocks);
maybeInit(this.stores.scorerDefinitions);
maybeInit(this.stores.mcpClients);
maybeInit(this.stores.mcpServers);
maybeInit(this.stores.workspaces);
maybeInit(this.stores.skills);
maybeInit(this.stores.favorites);
maybeInit(this.stores.blobs);
maybeInit(this.stores.backgroundTasks);
maybeInit(this.stores.schedules);
maybeInit(this.stores.channels);
}
await Promise.all(initTasks);
return true;
}
};
var MastraStorage = class extends MastraCompositeStore {
};
// src/storage/domains/base.ts
var StorageDomain = class extends MastraBase {
/**
* Initialize the storage domain.
* This should create any necessary tables/collections.
* Default implementation is a no-op - override in adapters that need initialization.
*/
async init() {
}
};
// src/storage/domains/versioned.ts
var ENTITY_ORDER_BY_SET = {
createdAt: true,
updatedAt: true
};
var SORT_DIRECTION_SET = {
ASC: true,
DESC: true
};
var VERSION_ORDER_BY_SET = {
versionNumber: true,
createdAt: true
};
var VersionedStorageDomain = class extends StorageDomain {
// ==========================================================================
// Concrete resolution methods
// ==========================================================================
/**
* Strips version metadata fields from a version row, leaving only snapshot config fields.
*/
extractSnapshotConfig(version) {
const result = {};
const metadataSet = new Set(this.versionMetadataFields);
for (const [key, value] of Object.entries(version)) {
if (!metadataSet.has(key)) {
result[key] = value;
}
}
return result;
}
/**
* Resolves an entity by merging its thin record with the active or latest version config.
* - `{ status: 'draft' }` — resolve with the latest version.
* - `{ status: 'published' }` (default) — resolve with the active version, falling back to latest.
* - `{ versionId: '...' }` — resolve with a specific version by ID.
*/
async getByIdResolved(id, options) {
const entity = await this.getById(id);
if (!entity) {
return null;
}
return this.resolveEntity(entity, options);
}
/**
* Lists entities with version resolution.
* When `status` is `'draft'`, each entity is resolved with its latest version.
* When `status` is `'published'` (default), each entity is resolved with its active version.
*/
async listResolved(args) {
const result = await this.list(args);
const status = args?.status;
const entities = result[this.listKey];
const resolved = await Promise.all(
entities.map((entity) => this.resolveEntity(entity, { status }))
);
return {
...result,
[this.listKey]: resolved
};
}
/**
* Resolves a single entity by merging it with its active or latest version.
* - `{ versionId: '...' }` — resolve with a specific version by ID.
* - `{ status: 'published' }` (default) — use activeVersionId, fall back to latest.
* - `{ status: 'draft' }` — always use the latest version.
*/
async resolveEntity(entity, options) {
const status = options?.status || "published";
let version = null;
if (options?.versionId) {
version = await this.getVersion(options.versionId);
} else if (status === "draft") {
version = await this.getLatestVersion(entity.id);
} else {
if (entity.activeVersionId) {
version = await this.getVersion(entity.activeVersionId);
if (!version) {
this.logger?.warn?.(
`Entity ${entity.id} has activeVersionId ${entity.activeVersionId} but version not found. Falling back to latest version.`
);
}
}
if (!version) {
version = await this.getLatestVersion(entity.id);
}
}
if (version) {
const snapshotConfig = this.extractSnapshotConfig(version);
return {
...entity,
...snapshotConfig,
resolvedVersionId: version.id
};
}
return entity;
}
// ==========================================================================
// Protected Helper Methods
// ==========================================================================
parseOrderBy(orderBy, defaultDirection = "DESC") {
return {
field: orderBy?.field && orderBy.field in ENTITY_ORDER_BY_SET ? orderBy.field : "createdAt",
direction: orderBy?.direction && orderBy.direction in SORT_DIRECTION_SET ? orderBy.direction : defaultDirection
};
}
parseVersionOrderBy(orderBy, defaultDirection = "DESC") {
return {
field: orderBy?.field && orderBy.field in VERSION_ORDER_BY_SET ? orderBy.field : "versionNumber",
direction: orderBy?.direction && orderBy.direction in SORT_DIRECTION_SET ? orderBy.direction : defaultDirection
};
}
};
var GitHistory = class {
/** Cache: dir → repo root (string) or `false` if not a repo. */
repoRootCache = /* @__PURE__ */ new Map();
/** Cache: `dir:filename:limit` → ordered commits (newest first). */
commitCache = /* @__PURE__ */ new Map();
/** Cache: `dir:commitHash:filename` → parsed JSON. */
snapshotCache = /* @__PURE__ */ new Map();
// ===========================================================================
// Public API
// ===========================================================================
/**
* Returns `true` if `dir` is inside a Git repository.
* Result is cached after the first call per directory.
*/
async isGitRepo(dir) {
const cached = this.repoRootCache.get(dir);
if (cached === false) return false;
if (typeof cached === "string") return true;
try {
const root = (await this.exec(dir, ["rev-parse", "--show-toplevel"])).trim();
this.repoRootCache.set(dir, root);
return true;
} catch {
this.repoRootCache.set(dir, false);
return false;
}
}
/**
* Get the list of commits that touched a specific file, newest first.
* Returns an empty array if Git is unavailable or the file has no history.
*
* @param dir Absolute path to the storage directory
* @param filename The JSON filename relative to `dir` (e.g., 'agents.json')
* @param limit Maximum number of commits to retrieve
*/
async getFileHistory(dir, filename, limit = 50) {
const cacheKey = `${dir}:${filename}:${limit}`;
if (this.commitCache.has(cacheKey)) {
return this.commitCache.get(cacheKey);
}
if (!await this.isGitRepo(dir)) {
this.commitCache.set(cacheKey, []);
return [];
}
try {
const raw = await this.exec(dir, [
"log",
`--max-count=${limit}`,
"--format=%H|%aI|%aN|%s",
"--follow",
"--",
filename
]);
const commits = [];
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const pipeIdx1 = trimmed.indexOf("|");
const pipeIdx2 = trimmed.indexOf("|", pipeIdx1 + 1);
const pipeIdx3 = trimmed.indexOf("|", pipeIdx2 + 1);
if (pipeIdx1 === -1 || pipeIdx2 === -1 || pipeIdx3 === -1) continue;
commits.push({
hash: trimmed.slice(0, pipeIdx1),
date: new Date(trimmed.slice(pipeIdx1 + 1, pipeIdx2)),
author: trimmed.slice(pipeIdx2 + 1, pipeIdx3),
message: trimmed.slice(pipeIdx3 + 1)
});
}
this.commitCache.set(cacheKey, commits);
return commits;
} catch {
this.commitCache.set(cacheKey, []);
return [];
}
}
/**
* Read and parse a JSON file at a specific Git commit.
* Returns the parsed entity map, or `null` if the file didn't exist at that commit.
*
* @param dir Absolute path to the storage directory
* @param commitHash Full or abbreviated commit SHA
* @param filename The JSON filename relative to `dir` (e.g., 'agents.json')
*/
async getFileAtCommit(dir, commitHash, filename) {
const cacheKey = `${dir}:${commitHash}:${filename}`;
if (this.snapshotCache.has(cacheKey)) {
return this.snapshotCache.get(cacheKey);
}
if (!await this.isGitRepo(dir)) return null;
try {
const relPath = this.relativeToRepo(dir, filename);
const raw = await this.exec(dir, ["show", `${commitHash}:${relPath}`]);
const parsed = JSON.parse(raw);
this.snapshotCache.set(cacheKey, parsed);
return parsed;
} catch {
return null;
}
}
/**
* Invalidate all caches. Call after external operations that change Git state
* (e.g., the user commits or pulls).
*/
invalidateCache() {
this.repoRootCache.clear();
this.commitCache.clear();
this.snapshotCache.clear();
}
// ===========================================================================
// Internals
// ===========================================================================
/**
* Get the relative path from the Git repo root to a file in the storage directory.
*/
relativeToRepo(dir, filename) {
const root = this.repoRootCache.get(dir);
if (!root) {
throw new Error(`Not a git repository: ${dir}`);
}
const realRoot = realpathSync(root);
const realDir = realpathSync(dir);
const relDir = relative(realRoot, realDir);
return relDir ? `${relDir}/${filename}` : filename;
}
/**
* Execute a git command and return stdout.
*/
exec(cwd, args) {
return new Promise((resolve, reject) => {
execFile("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
if (error) reject(error);
else resolve(stdout);
});
});
}
};
// src/storage/filesystem-versioned.ts
var GIT_VERSION_PREFIX = "git-";
var FilesystemVersionedHelpers = class _FilesystemVersionedHelpers {
db;
entitiesFile;
parentIdField;
name;
versionMetadataFields;
gitHistoryLimit;
/**
* In-memory entity records (thin metadata), keyed by entity ID.
*/
entities = /* @__PURE__ */ new Map();
/**
* In-memory version records, keyed by version ID.
* Includes both in-memory/hydrated versions and git-based versions (metadata only).
*/
versions = /* @__PURE__ */ new Map();
/**
* Whether we've loaded from disk yet.
*/
hydrated = false;
/**
* Git history utility instance (shared across all helpers).
*/
static gitHistory = new GitHistory();
/**
* Promise that resolves when git history has been loaded.
* null means git history loading hasn't been triggered yet.
*/
gitHistoryPromise = null;
/**
* The highest version number from git history, per entity ID.
* Used to assign version numbers to new in-memory versions that continue
* after the git history.
*/
gitVersionCounts = /* @__PURE__ */ new Map();
constructor(config) {
this.db = config.db;
this.entitiesFile = config.entitiesFile;
this.parentIdField = config.parentIdField;
this.name = config.name;
this.versionMetadataFields = config.versionMetadataFields;
this.gitHistoryLimit = config.gitHistoryLimit ?? 50;
}
/**
* Check if a version ID represents a git-based version.
*/
static isGitVersion(id) {
return id.startsWith(GIT_VERSION_PREFIX);
}
/**
* Hydrate in-memory state from the on-disk JSON file.
* For each entry on disk, creates an in-memory entity (status: 'published')
* and a synthetic version with the snapshot config.
*
* Also kicks off async git history loading in the background.
* Version numbers for hydrated entities are assigned as 1 initially,
* but will be reassigned after git history loads.
*/
hydrate() {
if (this.hydrated) return;
this.hydrated = true;
const diskData = this.db.readDomain(this.entitiesFile);
for (const [entityId, snapshotConfig] of Object.entries(diskData)) {
if (!snapshotConfig || typeof snapshotConfig !== "object") continue;
const versionId = `hydrated-${entityId}-v1`;
const now = /* @__PURE__ */ new Date();
const entity = {
id: entityId,
status: "published",
activeVersionId: versionId,
createdAt: now,
updatedAt: now
};
this.entities.set(entityId, entity);
const version = {
id: versionId,
[this.parentIdField]: entityId,
versionNumber: 1,
...snapshotConfig,
createdAt: now
};
this.versions.set(versionId, version);
}
this.gitHistoryPromise = this.loadGitHistory();
}
/**
* Ensure git history has been loaded before proceeding.
* Call this in version-related methods to ensure git versions are available.
*/
async ensureGitHistory() {
this.hydrate();
if (this.gitHistoryPromise) {
await this.gitHistoryPromise;
}
}
/**
* Load git commit history for the domain's JSON file.
* Creates read-only version records (metadata + snapshot config) for each
* commit where an entity existed. Reassigns version numbers for
* hydrated (current disk) versions to sit on top of git history.
*/
async loadGitHistory() {
const git = _FilesystemVersionedHelpers.gitHistory;
const dir = this.db.dir;
const isRepo = await git.isGitRepo(dir);
if (!isRepo) return;
const commits = await git.getFileHistory(dir, this.entitiesFile, this.gitHistoryLimit);
if (commits.length === 0) return;
const orderedCommits = [...commits].reverse();
const entityVersionCount = /* @__PURE__ */ new Map();
const previousSnapshots = /* @__PURE__ */ new Map();
for (let i = 0; i < orderedCommits.length; i++) {
const commit = orderedCommits[i];
const fileContent = await git.getFileAtCommit(
dir,
commit.hash,
this.entitiesFile
);
if (!fileContent) continue;
for (const [entityId, snapshotConfig] of Object.entries(fileContent)) {
if (!snapshotConfig || typeof snapshotConfig !== "object") continue;
const serialized = JSON.stringify(snapshotConfig);
if (previousSnapshots.get(entityId) === serialized) continue;
previousSnapshots.set(entityId, serialized);
const count = (entityVersionCount.get(entityId) ?? 0) + 1;
entityVersionCount.set(entityId, count);
const versionId = `${GIT_VERSION_PREFIX}${commit.hash}-${entityId}`;
if (this.versions.has(versionId)) continue;
const version = {
id: versionId,
[this.parentIdField]: entityId,
versionNumber: count,
changeMessage: commit.message,
...snapshotConfig,
createdAt: commit.date
};
this.versions.set(versionId, version);
}
}
this.gitVersionCounts = entityVersionCount;
for (const [entityId, gitCount] of entityVersionCount) {
const hydratedVersionId = `hydrated-${entityId}-v1`;
const version = this.versions.get(hydratedVersionId);
if (version) {
version.versionNumber = gitCount + 1;
}
}
}
// ==========================================================================
// Disk persistence — only published snapshot configs
// ==========================================================================
/**
* Write the published snapshot config for an entity to disk.
* Strips all entity metadata and version metadata fields, leaving only
* the clean primitive configuration.
*/
persistToDisk() {
const diskData = {};
for (const [entityId, entity] of this.entities) {
if (entity.status !== "published" || !entity.activeVersionId) continue;
const version = this.versions.get(entity.activeVersionId);
if (!version) continue;
const snapshotConfig = this.extractSnapshotConfig(version);
diskData[entityId] = snapshotConfig;
}
this.db.writeDomain(this.entitiesFile, diskData);
}
/**
* Extract the snapshot config from a version, stripping version metadata fields.
*/
extractSnapshotConfig(version) {
const metadataSet = new Set(this.versionMetadataFields);
const result = {};
for (const [key, value] of Object.entries(version)) {
if (!metadataSet.has(key)) {
result[key] = value;
}
}
return result;
}
// ==========================================================================
// Entity CRUD
// ==========================================================================
async getById(id) {
this.hydrate();
return this.entities.has(id) ? structuredClone(this.entities.get(id)) : null;
}
async createEntity(id, entity) {
this.hydrate();
if (this.entities.has(id)) {
throw new Error(`${this.name}: entity with id ${id} already exists`);
}
this.entities.set(id, structuredClone(entity));
return structuredClone(entity);
}
async updateEntity(id, updates) {
this.hydrate();
const existing = this.entities.get(id);
if (!existing) {
throw new Error(`${this.name}: entity with id ${id} not found`);
}
const updated = { ...existing };
for (const [key, value] of Object.entries(updates)) {
if (key === "id") continue;
if (value === void 0) continue;
if (key === "metadata" && typeof value === "object" && value !== null) {
updated["metadata"] = {
...updated["metadata"] ?? {},
...value
};
} else {
updated[key] = value;
}
}
updated["updatedAt"] = /* @__PURE__ */ new Date();
const updatedEntity = updated;
this.entities.set(id, structuredClone(updatedEntity));
const wasPublished = existing.status === "published";
const isPublished = updatedEntity.status === "published" && updatedEntity.activeVersionId;
if (isPublished || wasPublished && updates["status"] !== void 0) {
this.persistToDisk();
}
return structuredClone(updatedEntity);
}
async deleteEntity(id) {
this.hydrate();
this.entities.delete(id);
await this.deleteVersionsByParentId(id);
this.persistToDisk();
}
async listEntities(args) {
this.hydrate();
const { page = 0, perPage: perPageInput, orderBy, filters, listKey } = args;
const perPage = normalizePerPage(perPageInput, 100);
if (page < 0) throw new Error("page must be >= 0");
let entities = Array.from(this.entities.values());
if (filters) {
for (const [key, value] of Object.entries(filters)) {
if (value === void 0) continue;
if (key === "metadata" && typeof value === "object" && value !== null) {
entities = entities.filter((e) => {
const meta = e["metadata"];
if (!meta) return false;
return Object.entries(value).every(
([k, v]) => JSON.stringify(meta[k]) === JSON.stringify(v)
);
});
} else {
entities = entities.filter((e) => e[key] === value);
}
}
}
const field = orderBy?.field ?? "createdAt";
const direction = orderBy?.direction ?? "DESC";
entities.sort((a, b) => {
const aVal = new Date(a[field]).getTime();
const bVal = new Date(b[field]).getTime();
return direction === "ASC" ? aVal - bVal : bVal - aVal;
});
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
return {
[listKey]: entities.slice(offset, offset + perPage),
total: entities.length,
page,
perPage: perPageForResponse,
hasMore: offset + perPage < entities.length
};
}
// ==========================================================================
// Version Methods (in-memory + git history)
// ==========================================================================
async createVersion(input) {
await this.ensureGitHistory();
if (this.versions.has(input.id)) {
throw new Error(`${this.name}: version with id ${input.id} already exists`);
}
const parentId = input[this.parentIdField];
for (const v of this.versions.values()) {
if (v[this.parentIdField] === parentId && v.versionNumber === input.versionNumber) {
throw new Error(`${this.name}: version number ${input.versionNumber} already exists for entity ${parentId}`);
}
}
const version = {
...input,
createdAt: /* @__PURE__ */ new Date()
};
this.versions.set(input.id, structuredClone(version));
return structuredClone(version);
}
async getVersion(id) {
await this.ensureGitHistory();
return this.versions.has(id) ? structuredClone(this.versions.get(id)) : null;
}
async getVersionByNumber(entityId, versionNumber) {
await this.ensureGitHistory();
for (const v of this.versions.values()) {
if (v[this.parentIdField] === entityId && v.versionNumber === versionNumber) {
return structuredClone(v);
}
}
return null;
}
async getLatestVersion(entityId) {
await this.ensureGitHistory();
let latest = null;
for (const v of this.versions.values()) {
if (v[this.parentIdField] === entityId) {
if (!latest || v.versionNumber > latest.versionNumber) {
latest = v;
}
}
}
return latest ? structuredClone(latest) : null;
}
async listVersions(input, parentIdField) {
await this.ensureGitHistory();
const { page = 0, perPage: perPageInput, orderBy } = input;
const entityId = input[parentIdField];
const perPage = normalizePerPage(perPageInput, 20);
if (page < 0) throw new Error("page must be >= 0");
let versions = Array.from(this.versions.values()).filter(
(v) => v[this.parentIdField] === entityId
);
const field = orderBy?.field ?? "versionNumber";
const direction = orderBy?.direction ?? "DESC";
versions.sort((a, b) => {
const aVal = field === "createdAt" ? new Date(a.createdAt).getTime() : a.versionNumber;
const bVal = field === "createdAt" ? new Date(b.createdAt).getTime() : b.versionNumber;
return direction === "ASC" ? aVal - bVal : bVal - aVal;
});
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
return {
versions: versions.slice(offset, offset + perPage),
total: versions.length,
page,
perPage: perPageForResponse,
hasMore: offset + perPage < versions.length
};
}
async deleteVersion(id) {
await this.ensureGitHistory();
if (_FilesystemVersionedHelpers.isGitVersion(id)) return;
this.versions.delete(id);
}
async deleteVersionsByParentId(entityId) {
await this.ensureGitHistory();
for (const [versionId, version] of this.versions) {
if (version[this.parentIdField] === entityId) {
if (_FilesystemVersionedHelpers.isGitVersion(versionId)) continue;
this.versions.delete(versionId);
}
}
}
async countVersions(entityId) {
await this.ensureGitHistory();
let count = 0;
for (const v of this.versions.values()) {
if (v[this.parentIdField] === entityId) {
count++;
}
}
return count;
}
async getNextVersionNumber(entityId) {
await this.ensureGitHistory();
return this._getNextVersionNumber(entityId);
}
_getNextVersionNumber(entityId) {
const gitCount = this.gitVersionCounts.get(entityId) ?? 0;
let maxVersion = gitCount;
for (const v of this.versions.values()) {
if (v[this.parentIdField] === entityId) {
maxVersion = Math.max(maxVersion, v.versionNumber);
}
}
return maxVersion + 1;
}
async dangerouslyClearAll() {
this.entities.clear();
this.versions.clear();
this.gitVersionCounts.clear();
this.gitHistoryPromise = null;
this.hydrated = false;
this.db.clearDomain(this.entitiesFile);
}
};
export { EDITOR_DOMAINS, FilesystemVersionedHelpers, GitHistory, MastraCompositeStore, MastraStorage, StorageDomain, VersionedStorageDomain, calculatePagination, normalizePerPage };
//# sourceMappingURL=chunk-J5M7YSZZ.js.map
//# sourceMappingURL=chunk-J5M7YSZZ.js.map