@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
173 lines (171 loc) • 7.04 kB
JavaScript
import database_default from "../database/index.js";
import { transaction } from "../utils/transaction.js";
import { ItemsService } from "./items.js";
import { getExtensionManager } from "../extensions/index.js";
import { useEnv } from "@directus/env";
import { ForbiddenError, InvalidPayloadError, LimitExceededError, UnprocessableContentError } from "@directus/errors";
import { isObject } from "@directus/utils";
import { describe } from "@directus/extensions-registry";
//#region src/services/extensions.ts
var ExtensionReadError = class extends Error {
originalError;
constructor(originalError) {
super();
this.originalError = originalError;
}
};
var ExtensionsService = class ExtensionsService {
knex;
accountability;
schema;
extensionsItemService;
extensionsManager;
constructor(options) {
this.knex = options.knex || database_default();
this.schema = options.schema;
this.accountability = options.accountability || null;
this.extensionsManager = getExtensionManager();
this.extensionsItemService = new ItemsService("directus_extensions", {
knex: this.knex,
schema: this.schema,
accountability: this.accountability
});
}
async preInstall(extensionId, versionId) {
const env = useEnv();
const describeOptions = {};
if (typeof env["MARKETPLACE_REGISTRY"] === "string") describeOptions.registry = env["MARKETPLACE_REGISTRY"];
const extension = await describe(extensionId, describeOptions);
const version = extension.data.versions.find((version$1) => version$1.id === versionId);
if (!version) throw new ForbiddenError();
const limit = env["EXTENSIONS_LIMIT"] ? Number(env["EXTENSIONS_LIMIT"]) : null;
if (limit !== null) {
if (this.extensionsManager.extensions.length + (version.bundled.length ?? 1) >= limit) throw new LimitExceededError({ category: "Extensions" });
}
return {
extension,
version
};
}
async install(extensionId, versionId) {
const { extension, version } = await this.preInstall(extensionId, versionId);
await this.extensionsItemService.createOne({
id: extensionId,
enabled: true,
folder: versionId,
source: "registry",
bundle: null
});
if (extension.data.type === "bundle" && version.bundled.length > 0) await this.extensionsItemService.createMany(version.bundled.map((entry) => ({
enabled: true,
folder: entry.name,
source: "registry",
bundle: extensionId
})));
await this.extensionsManager.install(versionId);
}
async uninstall(id) {
const settings = await this.extensionsItemService.readOne(id);
if (settings.source !== "registry") throw new InvalidPayloadError({ reason: "Cannot uninstall extensions that were not installed via marketplace" });
if (settings.bundle !== null) throw new InvalidPayloadError({ reason: "Cannot uninstall sub extensions of bundles separately" });
await this.deleteOne(id);
await this.extensionsManager.uninstall(settings.folder);
}
async reinstall(id) {
const settings = await this.extensionsItemService.readOne(id);
if (settings.source !== "registry") throw new InvalidPayloadError({ reason: "Cannot reinstall extensions that were not installed via marketplace" });
if (settings.bundle !== null) throw new InvalidPayloadError({ reason: "Cannot reinstall sub extensions of bundles separately" });
const extensionId = settings.id;
const versionId = settings.folder;
await this.preInstall(extensionId, versionId);
await this.extensionsManager.install(versionId);
}
async readAll() {
await this.extensionsManager.isReloading();
const settings = await this.extensionsItemService.readByQuery({ limit: -1 });
const regular = settings.filter(({ bundle }) => bundle === null);
const bundled = settings.filter(({ bundle }) => bundle !== null);
const output = [];
for (const meta of regular) output.push({
id: meta.id,
bundle: meta.bundle,
meta,
schema: this.extensionsManager.getExtension(meta.source, meta.folder) ?? null
});
for (const meta of bundled) {
const parentBundle = output.find((ext) => ext.id === meta.bundle);
if (!parentBundle) continue;
const schema = parentBundle.schema?.entries.find((entry) => entry.name === meta.folder);
if (!schema) continue;
output.push({
id: meta.id,
bundle: meta.bundle,
meta,
schema
});
}
return output;
}
async readOne(id) {
await this.extensionsManager.isReloading();
const meta = await this.extensionsItemService.readOne(id);
const schema = this.extensionsManager.getExtension(meta.source, meta.folder) ?? null;
return {
id: meta.id,
bundle: meta.bundle,
schema,
meta
};
}
async updateOne(id, data) {
const result = await transaction(this.knex, async (trx) => {
if (!isObject(data.meta)) throw new InvalidPayloadError({ reason: `"meta" is required` });
const service = new ExtensionsService({
knex: trx,
accountability: this.accountability,
schema: this.schema
});
await service.extensionsItemService.updateOne(id, data.meta);
let extension;
try {
extension = await service.readOne(id);
} catch (error) {
throw new ExtensionReadError(error);
}
if ("enabled" in data.meta) await service.checkBundleAndSyncStatus(trx, id, extension);
return extension;
});
this.extensionsManager.reload().then(() => {
this.extensionsManager.broadcastReloadNotification();
});
return result;
}
async deleteOne(id) {
await this.extensionsItemService.deleteOne(id);
await this.extensionsItemService.deleteByQuery({ filter: { bundle: { _eq: id } } });
}
/**
* Sync a bundles enabled status
* - If the extension or extensions parent is not a bundle changes are skipped
* - If a bundles status is toggled, all children are set to that status
* - If an entries status is toggled, then if the:
* - Parent bundle is non-partial throws UnprocessableContentError
* - Entry status change resulted in all children being disabled then the parent bundle is disabled
* - Entry status change resulted in at least one child being enabled then the parent bundle is enabled
*/
async checkBundleAndSyncStatus(trx, extensionId, extension) {
if (extension.bundle === null && extension.schema?.type === "bundle") {
await trx("directus_extensions").update({ enabled: extension.meta.enabled }).where({ bundle: extensionId }).orWhere({ id: extensionId });
return;
}
const parentId = extension.bundle ?? extension.meta.bundle;
if (!parentId) return;
const parent = await this.readOne(parentId);
if (parent.schema?.type !== "bundle") return;
if (parent.schema.partial === false) throw new UnprocessableContentError({ reason: "Unable to toggle status of an entry for a bundle marked as non partial" });
if (!!await trx("directus_extensions").where({ bundle: parentId }).where({ enabled: true }).first()) await trx("directus_extensions").update({ enabled: true }).where({ id: parentId });
else await trx("directus_extensions").update({ enabled: false }).where({ id: parentId });
}
};
//#endregion
export { ExtensionReadError, ExtensionsService };