@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
303 lines (301 loc) • 12.5 kB
JavaScript
import { useRedis } from "../redis/lib/use-redis.js";
import { redisConfigAvailable } from "../redis/utils/redis-config-available.js";
import "../redis/index.js";
import { useLogger } from "../logger/index.js";
import { buildProviderConfigs } from "../ai/providers/registry.js";
import { getMilliseconds } from "../utils/get-milliseconds.js";
import { FILE_UPLOADS, RESUMABLE_UPLOADS } from "../constants.js";
import { getStorage } from "../storage/index.js";
import database_default, { hasDatabaseConnection } from "../database/index.js";
import getMailer from "../mailer.js";
import { getEntitlementManager } from "../license/entitlements/manager.js";
import { isUnauthenticated } from "../utils/is-unauthenticated.js";
import { SettingsService } from "./settings.js";
import { useStore } from "../utils/store.js";
import { getAllowedLogLevels } from "../utils/get-allowed-log-levels.js";
import { SERVER_ONLINE } from "../server.js";
import { getLicenseManager } from "../license/manager.js";
import "../license/index.js";
import { useEnv } from "@directus/env";
import { ForbiddenError } from "@directus/errors";
import { toArray, toBoolean } from "@directus/utils";
import { merge } from "lodash-es";
import { createKv } from "@directus/memory";
import { performance } from "perf_hooks";
import { Readable } from "node:stream";
import { version } from "directus/version";
//#region src/services/server.ts
const env = useEnv();
const logger = useLogger();
const HEALTHCHECK_CACHE_TTL = getMilliseconds(env["HEALTHCHECK_CACHE_TTL"], 3e5);
const store = useStore(env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck", { ttl: HEALTHCHECK_CACHE_TTL });
var ServerService = class {
knex;
accountability;
settingsService;
schema;
constructor(options) {
this.knex = options.knex || database_default();
this.accountability = options.accountability || null;
this.schema = options.schema;
this.settingsService = new SettingsService({
knex: this.knex,
schema: this.schema
});
}
async isSetupCompleted() {
return Boolean(await this.knex("directus_users").first());
}
async serverInfo() {
const info = {};
const licenseManager = getLicenseManager();
const isSetupCompleted = await this.isSetupCompleted();
const { project_owner,...projectInfo } = await this.settingsService.readSingleton({ fields: [
"project_name",
"project_owner",
"project_descriptor",
"project_logo",
"project_color",
"default_appearance",
"default_theme_light",
"default_theme_dark",
"theme_light_overrides",
"theme_dark_overrides",
"default_language",
"public_foreground",
"public_background.id",
"public_background.type",
"public_favicon",
"public_note",
"custom_css",
"public_registration",
"public_registration_verify_email"
] });
info["project"] = projectInfo;
if (!isSetupCompleted) info["setup"] = {
license_complete: licenseManager.getSource() !== null,
owner_complete: Boolean(project_owner)
};
if (this.accountability?.user) {
info["project_owner_enabled"] = toBoolean(env["PROJECT_OWNER_ENABLED"] ?? true);
info["mcp_enabled"] = toBoolean(env["MCP_ENABLED"] ?? true);
info["ai_enabled"] = toBoolean(env["AI_ENABLED"] ?? true);
info["mcp_oauth_enabled"] = toBoolean(env["MCP_OAUTH_ENABLED"] ?? false);
info["mcp_oauth_dcr_enabled"] = toBoolean(env["MCP_OAUTH_DCR_ENABLED"] ?? false);
info["mcp_oauth_cimd_enabled"] = toBoolean(env["MCP_OAUTH_CIMD_ENABLED"] ?? false);
info["autoSave"] = { revisionInterval: Number(env["AUTOSAVE_REVISION_INTERVAL"]) };
if (info["ai_enabled"]) {
const aiSettings = await this.settingsService.readSingleton({ fields: [
"ai_openai_api_key",
"ai_anthropic_api_key",
"ai_google_api_key",
"ai_openai_compatible_api_key",
"ai_openai_compatible_base_url"
] });
info["ai_providers"] = buildProviderConfigs({
openaiApiKey: aiSettings["ai_openai_api_key"] ?? null,
anthropicApiKey: aiSettings["ai_anthropic_api_key"] ?? null,
googleApiKey: aiSettings["ai_google_api_key"] ?? null,
openaiCompatibleApiKey: aiSettings["ai_openai_compatible_api_key"] ?? null,
openaiCompatibleBaseUrl: aiSettings["ai_openai_compatible_base_url"] ?? null
}).map((config) => config.type);
}
info["files"] = { mimeTypeAllowList: toArray(env["FILES_MIME_TYPE_ALLOW_LIST"]) };
if (env["RATE_LIMITER_ENABLED"]) info["rateLimit"] = {
points: env["RATE_LIMITER_POINTS"],
duration: env["RATE_LIMITER_DURATION"]
};
else info["rateLimit"] = false;
if (env["RATE_LIMITER_GLOBAL_ENABLED"]) info["rateLimitGlobal"] = {
points: env["RATE_LIMITER_GLOBAL_POINTS"],
duration: env["RATE_LIMITER_GLOBAL_DURATION"]
};
else info["rateLimitGlobal"] = false;
info["extensions"] = { limit: env["EXTENSIONS_LIMIT"] ?? null };
info["queryLimit"] = {
default: env["QUERY_LIMIT_DEFAULT"],
max: Number.isFinite(env["QUERY_LIMIT_MAX"]) ? env["QUERY_LIMIT_MAX"] : -1
};
if (toBoolean(env["WEBSOCKETS_ENABLED"])) {
info["websocket"] = {};
info["websocket"].rest = toBoolean(env["WEBSOCKETS_REST_ENABLED"]) ? {
authentication: env["WEBSOCKETS_REST_AUTH"],
path: env["WEBSOCKETS_REST_PATH"]
} : false;
info["websocket"].graphql = toBoolean(env["WEBSOCKETS_GRAPHQL_ENABLED"]) ? {
authentication: env["WEBSOCKETS_GRAPHQL_AUTH"],
path: env["WEBSOCKETS_GRAPHQL_PATH"]
} : false;
info["websocket"].heartbeat = toBoolean(env["WEBSOCKETS_HEARTBEAT_ENABLED"]) ? env["WEBSOCKETS_HEARTBEAT_PERIOD"] : false;
info["websocket"].collaborativeEditing = toBoolean(env["WEBSOCKETS_COLLAB_ENABLED"]);
info["websocket"].logs = toBoolean(env["WEBSOCKETS_LOGS_ENABLED"]) && this.accountability.admin ? { allowedLogLevels: getAllowedLogLevels(env["WEBSOCKETS_LOGS_LEVEL"] || "info") } : false;
} else info["websocket"] = false;
if (FILE_UPLOADS.MAX_CONCURRENCY && FILE_UPLOADS.MAX_CONCURRENCY !== Infinity) info["uploads"] = { maxConcurrency: FILE_UPLOADS.MAX_CONCURRENCY };
if (RESUMABLE_UPLOADS.ENABLED) info["uploads"] = {
...info["uploads"],
tus: true,
chunkSize: RESUMABLE_UPLOADS.CHUNK_SIZE
};
}
if (this.accountability?.user || !isSetupCompleted) info["version"] = version;
info["license"] = {
source: licenseManager.getSource(),
entitlements: getEntitlementManager().getAppEntitlements()
};
return info;
}
async health() {
if (isUnauthenticated(this.accountability)) throw new ForbiddenError();
const healthResult = await store(async (store$1) => {
try {
return await store$1.get("health");
} catch (err) {
logger.warn(err, "Failed to read health check cache");
}
});
if (healthResult) return this.accountability?.admin === true ? healthResult : { status: healthResult["status"] };
const { nanoid } = await import("nanoid");
const checkID = nanoid(5);
const enabledServices = toArray(env["HEALTHCHECK_SERVICES"]);
const data = {
status: "ok",
releaseId: version,
serviceId: env["PUBLIC_URL"],
checks: merge(...await Promise.all([
testDatabase(),
testRedis(),
testStorage(),
testEmail()
]))
};
if (SERVER_ONLINE === false) data.status = "error";
for (const [service, healthData] of Object.entries(data.checks)) {
for (const healthCheck of healthData) {
if (healthCheck.status === "warn" && data.status === "ok") {
logger.warn(`${service} in WARN state, the observed value ${healthCheck.observedValue} is above the threshold of ${healthCheck.threshold}${healthCheck.observedUnit}`);
data.status = "warn";
continue;
}
if (healthCheck.status === "error" && (data.status === "ok" || data.status === "warn")) {
logger.error(healthCheck.output, "%s in ERROR state", service);
data.status = "error";
break;
}
}
if (data.status === "error") break;
}
await store(async (store$1) => {
await store$1.set("health", data).catch((err) => {
logger.warn(err, "Failed to write health check cache");
});
});
return this.accountability?.admin === true ? data : { status: data.status };
async function testDatabase() {
if (enabledServices.includes("database") === false) return {};
const database = database_default();
const client = env["DB_CLIENT"];
const checks = {};
checks[`${client}:responseTime`] = [{
status: "ok",
componentType: "datastore",
observedUnit: "ms",
observedValue: 0,
threshold: env["DB_HEALTHCHECK_THRESHOLD"] ? +env["DB_HEALTHCHECK_THRESHOLD"] : 150
}];
const startTime = performance.now();
if (await hasDatabaseConnection()) checks[`${client}:responseTime`][0].status = "ok";
else {
checks[`${client}:responseTime`][0].status = "error";
checks[`${client}:responseTime`][0].output = `Can't connect to the database.`;
}
const endTime = performance.now();
checks[`${client}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3);
if (Number(checks[`${client}:responseTime`][0].observedValue) > checks[`${client}:responseTime`][0].threshold && checks[`${client}:responseTime`][0].status !== "error") checks[`${client}:responseTime`][0].status = "warn";
checks[`${client}:connectionsAvailable`] = [{
status: "ok",
componentType: "datastore",
observedValue: database.client.pool.numFree()
}];
checks[`${client}:connectionsUsed`] = [{
status: "ok",
componentType: "datastore",
observedValue: database.client.pool.numUsed()
}];
return checks;
}
async function testRedis() {
if (enabledServices.includes("redis") === false || redisConfigAvailable() !== true) return {};
const redis = createKv({
type: "redis",
redis: useRedis(),
namespace: env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck",
ttl: HEALTHCHECK_CACHE_TTL
});
const checks = { "redis:responseTime": [{
status: "ok",
componentType: "cache",
observedValue: 0,
observedUnit: "ms",
threshold: env["CACHE_HEALTHCHECK_THRESHOLD"] ? +env["CACHE_HEALTHCHECK_THRESHOLD"] : 150
}] };
const startTime = performance.now();
try {
await redis.set(`directus-health-${checkID}`, 1);
await redis.delete(`directus-health-${checkID}`);
} catch (err) {
checks["redis:responseTime"][0].status = "error";
checks["redis:responseTime"][0].output = err;
} finally {
const endTime = performance.now();
checks["redis:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
if (checks["redis:responseTime"][0].observedValue > checks["redis:responseTime"][0].threshold && checks["redis:responseTime"][0].status !== "error") checks["redis:responseTime"][0].status = "warn";
}
return checks;
}
async function testStorage() {
if (enabledServices.includes("storage") === false) return {};
const storage = await getStorage();
const checks = {};
for (const location of toArray(env["STORAGE_LOCATIONS"])) {
const disk = storage.location(location);
const envThresholdKey = `STORAGE_${location}_HEALTHCHECK_THRESHOLD`.toUpperCase();
checks[`storage:${location}:responseTime`] = [{
status: "ok",
componentType: "objectstore",
observedValue: 0,
observedUnit: "ms",
threshold: env[envThresholdKey] ? +env[envThresholdKey] : 750
}];
const startTime = performance.now();
try {
await disk.write("directus-health-file", Readable.from([checkID]));
} catch (err) {
checks[`storage:${location}:responseTime`][0].status = "error";
checks[`storage:${location}:responseTime`][0].output = err;
} finally {
const endTime = performance.now();
checks[`storage:${location}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3);
if (Number(checks[`storage:${location}:responseTime`][0].observedValue) > checks[`storage:${location}:responseTime`][0].threshold && checks[`storage:${location}:responseTime`][0].status !== "error") checks[`storage:${location}:responseTime`][0].status = "warn";
}
}
return checks;
}
async function testEmail() {
if (enabledServices.includes("email") === false || toBoolean(env["EMAIL_VERIFY_SETUP"]) === false) return {};
const checks = { "email:connection": [{
status: "ok",
componentType: "email"
}] };
const mailer = getMailer();
try {
await mailer.verify();
} catch (err) {
checks["email:connection"][0].status = "error";
checks["email:connection"][0].output = err;
}
return checks;
}
}
};
//#endregion
export { ServerService };