@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
528 lines (525 loc) • 16.9 kB
JavaScript
import { VersionedStorageDomain, normalizePerPage, calculatePagination, FilesystemVersionedHelpers } from './chunk-J5M7YSZZ.js';
import { randomUUID } from 'crypto';
// src/storage/domains/skills/base.ts
var SkillsStorage = class extends VersionedStorageDomain {
listKey = "skills";
versionMetadataFields = [
"id",
"skillId",
"versionNumber",
"changedFields",
"changeMessage",
"createdAt"
];
constructor() {
super({
component: "STORAGE",
name: "SKILLS"
});
}
};
var InMemorySkillsStorage = class extends SkillsStorage {
db;
constructor({ db }) {
super();
this.db = db;
}
async dangerouslyClearAll() {
this.db.skills.clear();
this.db.skillVersions.clear();
}
// ==========================================================================
// Skill CRUD Methods
// ==========================================================================
async getById(id) {
const config = this.db.skills.get(id);
return config ? this.deepCopyConfig(config) : null;
}
async create(input) {
const { skill } = input;
if (this.db.skills.has(skill.id)) {
throw new Error(`Skill with id ${skill.id} already exists`);
}
const now = /* @__PURE__ */ new Date();
const visibility = skill.visibility ?? (skill.authorId ? "private" : void 0);
const newConfig = {
id: skill.id,
status: "draft",
activeVersionId: void 0,
authorId: skill.authorId,
visibility,
favoriteCount: 0,
createdAt: now,
updatedAt: now
};
this.db.skills.set(skill.id, newConfig);
const { id: _id, authorId: _authorId, visibility: _visibility, ...snapshotConfig } = skill;
const versionId = randomUUID();
try {
await this.createVersion({
id: versionId,
skillId: skill.id,
versionNumber: 1,
...snapshotConfig,
changedFields: Object.keys(snapshotConfig),
changeMessage: "Initial version"
});
} catch (error) {
this.db.skills.delete(skill.id);
throw error;
}
return this.deepCopyConfig(newConfig);
}
async update(input) {
const { id, ...updates } = input;
const existingConfig = this.db.skills.get(id);
if (!existingConfig) {
throw new Error(`Skill with id ${id} not found`);
}
const { authorId, visibility, activeVersionId, status, ...rawConfigFields } = updates;
const configFields = {};
for (const [key, value] of Object.entries(rawConfigFields)) {
if (value !== void 0) configFields[key] = value;
}
const configFieldNames = [
"name",
"description",
"instructions",
"license",
"compatibility",
"source",
"references",
"scripts",
"assets",
"files",
"metadata",
"tree"
];
const hasConfigUpdate = configFieldNames.some((field) => field in configFields);
const updatedConfig = {
...existingConfig,
...authorId !== void 0 && { authorId },
...visibility !== void 0 && { visibility },
...activeVersionId !== void 0 && { activeVersionId },
...status !== void 0 && { status },
updatedAt: /* @__PURE__ */ new Date()
};
if (activeVersionId !== void 0 && status === void 0) {
updatedConfig.status = "published";
}
if (hasConfigUpdate) {
const latestVersion = await this.getLatestVersion(id);
if (!latestVersion) {
throw new Error(`No versions found for skill ${id}`);
}
const {
id: _versionId,
skillId: _skillId,
versionNumber: _versionNumber,
changedFields: _changedFields,
changeMessage: _changeMessage,
createdAt: _createdAt,
...latestConfig
} = latestVersion;
const newConfig = {
...latestConfig,
...configFields
};
const changedFields = configFieldNames.filter(
(field) => field in configFields && JSON.stringify(configFields[field]) !== JSON.stringify(latestConfig[field])
);
if (changedFields.length > 0) {
const newVersionId = randomUUID();
const newVersionNumber = latestVersion.versionNumber + 1;
await this.createVersion({
id: newVersionId,
skillId: id,
versionNumber: newVersionNumber,
...newConfig,
changedFields,
changeMessage: `Updated ${changedFields.join(", ")}`
});
}
}
this.db.skills.set(id, updatedConfig);
return this.deepCopyConfig(updatedConfig);
}
async delete(id) {
this.db.skills.delete(id);
await this.deleteVersionsByParentId(id);
}
async list(args) {
const {
page = 0,
perPage: perPageInput,
orderBy,
authorId,
status,
visibility,
metadata,
entityIds,
pinFavoritedFor,
favoritedOnly
} = args || {};
const { field, direction } = this.parseOrderBy(orderBy);
const perPage = normalizePerPage(perPageInput, 100);
if (page < 0) {
throw new Error("page must be >= 0");
}
const maxOffset = Number.MAX_SAFE_INTEGER / 2;
if (page * perPage > maxOffset) {
throw new Error("page value too large");
}
let configs = Array.from(this.db.skills.values());
if (entityIds !== void 0) {
if (entityIds.length === 0) {
return {
skills: [],
total: 0,
page,
perPage: perPageInput === false ? false : perPage,
hasMore: false
};
}
const idSet = new Set(entityIds);
configs = configs.filter((config) => idSet.has(config.id));
}
if (authorId !== void 0) {
configs = configs.filter((config) => config.authorId === authorId);
}
if (status !== void 0) {
configs = configs.filter((config) => config.status === status);
}
if (visibility !== void 0) {
configs = configs.filter((config) => config.visibility === visibility);
}
if (metadata && Object.keys(metadata).length > 0) {
configs = configs.filter((_config) => {
return false;
});
}
const favoritedIds = pinFavoritedFor ? this.collectFavoritedIdsFor(pinFavoritedFor) : void 0;
if (favoritedOnly) {
if (favoritedIds) {
configs = configs.filter((config) => favoritedIds.has(config.id));
} else {
configs = [];
}
}
const sortedConfigs = this.sortConfigs(configs, field, direction, favoritedIds);
const clonedConfigs = sortedConfigs.map((config) => this.deepCopyConfig(config));
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
return {
skills: clonedConfigs.slice(offset, offset + perPage),
total: clonedConfigs.length,
page,
perPage: perPageForResponse,
hasMore: offset + perPage < clonedConfigs.length
};
}
// ==========================================================================
// Skill Version Methods
// ==========================================================================
async createVersion(input) {
if (this.db.skillVersions.has(input.id)) {
throw new Error(`Version with id ${input.id} already exists`);
}
for (const version2 of this.db.skillVersions.values()) {
if (version2.skillId === input.skillId && version2.versionNumber === input.versionNumber) {
throw new Error(`Version number ${input.versionNumber} already exists for skill ${input.skillId}`);
}
}
const version = {
...input,
createdAt: /* @__PURE__ */ new Date()
};
this.db.skillVersions.set(input.id, this.deepCopyVersion(version));
return this.deepCopyVersion(version);
}
async getVersion(id) {
const version = this.db.skillVersions.get(id);
return version ? this.deepCopyVersion(version) : null;
}
async getVersionByNumber(skillId, versionNumber) {
for (const version of this.db.skillVersions.values()) {
if (version.skillId === skillId && version.versionNumber === versionNumber) {
return this.deepCopyVersion(version);
}
}
return null;
}
async getLatestVersion(skillId) {
let latest = null;
for (const version of this.db.skillVersions.values()) {
if (version.skillId === skillId) {
if (!latest || version.versionNumber > latest.versionNumber) {
latest = version;
}
}
}
return latest ? this.deepCopyVersion(latest) : null;
}
async listVersions(input) {
const { skillId, page = 0, perPage: perPageInput, orderBy } = input;
const { field, direction } = this.parseVersionOrderBy(orderBy);
const perPage = normalizePerPage(perPageInput, 20);
if (page < 0) {
throw new Error("page must be >= 0");
}
const maxOffset = Number.MAX_SAFE_INTEGER / 2;
if (page * perPage > maxOffset) {
throw new Error("page value too large");
}
let versions = Array.from(this.db.skillVersions.values()).filter((v) => v.skillId === skillId);
versions = this.sortVersions(versions, field, direction);
const clonedVersions = versions.map((v) => this.deepCopyVersion(v));
const total = clonedVersions.length;
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
const paginatedVersions = clonedVersions.slice(offset, offset + perPage);
return {
versions: paginatedVersions,
total,
page,
perPage: perPageForResponse,
hasMore: offset + perPage < total
};
}
async deleteVersion(id) {
this.db.skillVersions.delete(id);
}
async deleteVersionsByParentId(entityId) {
const idsToDelete = [];
for (const [id, version] of this.db.skillVersions.entries()) {
if (version.skillId === entityId) {
idsToDelete.push(id);
}
}
for (const id of idsToDelete) {
this.db.skillVersions.delete(id);
}
}
async countVersions(skillId) {
let count = 0;
for (const version of this.db.skillVersions.values()) {
if (version.skillId === skillId) {
count++;
}
}
return count;
}
// ==========================================================================
// Private Helper Methods
// ==========================================================================
deepCopyConfig(config) {
return {
...config
};
}
deepCopyVersion(version) {
return structuredClone(version);
}
sortConfigs(configs, field, direction, favoritedIds) {
return configs.sort((a, b) => {
if (favoritedIds) {
const aFav = favoritedIds.has(a.id) ? 1 : 0;
const bFav = favoritedIds.has(b.id) ? 1 : 0;
if (aFav !== bFav) return bFav - aFav;
}
const aValue = a[field].getTime();
const bValue = b[field].getTime();
if (aValue !== bValue) {
return direction === "ASC" ? aValue - bValue : bValue - aValue;
}
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
});
}
/**
* Collect the set of skill IDs favorited by the given user. Returns an empty
* Set when the favorites domain is not wired or the user has no favorites.
*/
collectFavoritedIdsFor(userId) {
const favorited = /* @__PURE__ */ new Set();
for (const row of this.db.favorites.values()) {
if (row.userId === userId && row.entityType === "skill") {
favorited.add(row.entityId);
}
}
return favorited;
}
sortVersions(versions, field, direction) {
return versions.sort((a, b) => {
let aVal;
let bVal;
if (field === "createdAt") {
aVal = a.createdAt.getTime();
bVal = b.createdAt.getTime();
} else {
aVal = a.versionNumber;
bVal = b.versionNumber;
}
return direction === "ASC" ? aVal - bVal : bVal - aVal;
});
}
};
// src/storage/domains/skills/filesystem.ts
var FilesystemSkillsStorage = class extends SkillsStorage {
helpers;
constructor({ db }) {
super();
this.helpers = new FilesystemVersionedHelpers({
db,
entitiesFile: "skills.json",
parentIdField: "skillId",
name: "FilesystemSkillsStorage",
versionMetadataFields: ["id", "skillId", "versionNumber", "changedFields", "changeMessage", "createdAt"]
});
}
async init() {
await this.helpers.db.init();
}
async dangerouslyClearAll() {
await this.helpers.dangerouslyClearAll();
}
async getById(id) {
return this.helpers.getById(id);
}
async create(input) {
const { skill } = input;
const now = /* @__PURE__ */ new Date();
const visibility = skill.visibility ?? (skill.authorId ? "private" : void 0);
const entity = {
id: skill.id,
status: "draft",
activeVersionId: void 0,
authorId: skill.authorId,
visibility,
createdAt: now,
updatedAt: now
};
await this.helpers.createEntity(skill.id, entity);
const { id: _id, authorId: _authorId, visibility: _visibility, ...snapshotConfig } = skill;
const versionId = crypto.randomUUID();
await this.createVersion({
id: versionId,
skillId: skill.id,
versionNumber: 1,
...snapshotConfig,
changedFields: Object.keys(snapshotConfig),
changeMessage: "Initial version"
});
return structuredClone(entity);
}
async update(input) {
const { id, ...updates } = input;
const existing = await this.helpers.getById(id);
if (!existing) {
throw new Error(`FilesystemSkillsStorage: skill with id ${id} not found`);
}
const { authorId, visibility, activeVersionId, status, ...rawConfigFields } = updates;
const configFields = {};
for (const [key, value] of Object.entries(rawConfigFields)) {
if (value !== void 0) configFields[key] = value;
}
const configFieldNames = [
"name",
"description",
"instructions",
"license",
"compatibility",
"source",
"references",
"scripts",
"assets",
"files",
"metadata",
"tree"
];
const hasConfigUpdate = configFieldNames.some((field) => field in configFields);
if (hasConfigUpdate) {
const latestVersion = await this.getLatestVersion(id);
if (!latestVersion) {
throw new Error(`No versions found for skill ${id}`);
}
const {
id: _versionId,
skillId: _skillId,
versionNumber: _versionNumber,
changedFields: _changedFields,
changeMessage: _changeMessage,
createdAt: _createdAt,
...latestConfig
} = latestVersion;
const newConfig = {
...latestConfig,
...configFields
};
const changedFields = configFieldNames.filter(
(field) => field in configFields && JSON.stringify(configFields[field]) !== JSON.stringify(latestConfig[field])
);
if (changedFields.length > 0) {
const newVersionId = crypto.randomUUID();
const newVersionNumber = latestVersion.versionNumber + 1;
await this.createVersion({
id: newVersionId,
skillId: id,
versionNumber: newVersionNumber,
...newConfig,
changedFields,
changeMessage: `Updated ${changedFields.join(", ")}`
});
}
}
const entityUpdates = {
...authorId !== void 0 && { authorId },
...visibility !== void 0 && { visibility },
...activeVersionId !== void 0 && { activeVersionId },
...status !== void 0 && { status }
};
if (activeVersionId !== void 0 && status === void 0) {
entityUpdates.status = "published";
}
return await this.helpers.updateEntity(id, entityUpdates);
}
async delete(id) {
await this.helpers.deleteEntity(id);
}
async list(args) {
const { page, perPage, orderBy, authorId, visibility, metadata } = args || {};
const result = await this.helpers.listEntities({
page,
perPage,
orderBy,
listKey: "skills",
filters: { authorId, visibility, metadata }
});
return result;
}
async createVersion(input) {
return this.helpers.createVersion(input);
}
async getVersion(id) {
return this.helpers.getVersion(id);
}
async getVersionByNumber(skillId, versionNumber) {
return this.helpers.getVersionByNumber(skillId, versionNumber);
}
async getLatestVersion(skillId) {
return this.helpers.getLatestVersion(skillId);
}
async listVersions(input) {
const result = await this.helpers.listVersions(input, "skillId");
return result;
}
async deleteVersion(id) {
await this.helpers.deleteVersion(id);
}
async deleteVersionsByParentId(entityId) {
await this.helpers.deleteVersionsByParentId(entityId);
}
async countVersions(skillId) {
return this.helpers.countVersions(skillId);
}
};
export { FilesystemSkillsStorage, InMemorySkillsStorage, SkillsStorage };
//# sourceMappingURL=chunk-LMVYLFGE.js.map
//# sourceMappingURL=chunk-LMVYLFGE.js.map