@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
203 lines (202 loc) • 7.74 kB
JavaScript
import { useEnv } from '@directus/env';
import { InvalidPayloadError, InvalidProviderConfigError } from '@directus/errors';
import { mergeFilters, parseJSON } from '@directus/utils';
import { has, isEmpty } from 'lodash-es';
import { getCache, getCacheValueWithTTL, setCacheValueWithExpiry } from '../cache.js';
import { getDeploymentDriver } from '../deployment.js';
import { getMilliseconds } from '../utils/get-milliseconds.js';
import { ItemsService } from './items.js';
const env = useEnv();
const DEPLOYMENT_CACHE_TTL = getMilliseconds(env['CACHE_DEPLOYMENT_TTL']) || 5000; // Default 5s
export class DeploymentService extends ItemsService {
constructor(options) {
super('directus_deployments', options);
}
async createOne(data, opts) {
const provider = data.provider;
if (!provider) {
throw new InvalidPayloadError({ reason: 'Provider is required' });
}
if (isEmpty(data.credentials)) {
throw new InvalidPayloadError({ reason: 'Credentials are required' });
}
let credentials;
try {
credentials = this.parseValue(data.credentials, {});
}
catch {
throw new InvalidPayloadError({ reason: 'Credentials must be valid JSON' });
}
let options;
try {
options = this.parseValue(data.options, undefined);
}
catch {
throw new InvalidPayloadError({ reason: 'Options must be valid JSON' });
}
// Test connection before persisting
const driver = getDeploymentDriver(provider, credentials, options);
try {
await driver.testConnection();
}
catch {
throw new InvalidProviderConfigError({ provider, reason: 'Invalid config connection' });
}
const payload = {
...data,
// Persist as string so payload service encrypts the value
credentials: JSON.stringify(credentials),
};
if (!isEmpty(options)) {
payload.options = JSON.stringify(options);
}
return super.createOne(payload, opts);
}
async updateOne(key, data, opts) {
const hasCredentials = has(data, 'credentials');
const hasOptions = has(data, 'options');
if (!hasCredentials && !hasOptions) {
return super.updateOne(key, data, opts);
}
const existing = await this.readOne(key);
const provider = existing.provider;
const internal = await this.readConfig(provider);
let credentials = this.parseValue(internal.credentials, {});
if (hasCredentials) {
try {
const parsed = this.parseValue(data.credentials, {});
credentials = { ...credentials, ...parsed };
}
catch {
throw new InvalidPayloadError({ reason: 'Credentials must be valid JSON or object' });
}
}
let options = existing.options ?? undefined;
if (hasOptions) {
try {
options = this.parseValue(data.options, undefined);
}
catch {
throw new InvalidPayloadError({ reason: 'Options must be valid JSON' });
}
if (isEmpty(options)) {
throw new InvalidPayloadError({ reason: 'Options must not be empty' });
}
}
// Test connection before persisting
const driver = getDeploymentDriver(provider, credentials, options);
try {
await driver.testConnection();
}
catch {
throw new InvalidProviderConfigError({ provider, reason: 'Invalid config connection' });
}
return super.updateOne(key, {
credentials: JSON.stringify(credentials),
...(!isEmpty(options) ? { options: JSON.stringify(options) } : {}),
}, opts);
}
/**
* Read deployment config by provider
*/
async readByProvider(provider, query) {
const results = await this.readByQuery({
...query,
filter: mergeFilters({ provider: { _eq: provider } }, query?.filter ?? null),
limit: 1,
});
if (!results || results.length === 0) {
throw new Error(`Deployment config for "${provider}" not found`);
}
return results[0];
}
/**
* Update deployment config by provider
*/
async updateByProvider(provider, data) {
const deployment = await this.readByProvider(provider);
return this.updateOne(deployment.id, data);
}
/**
* Delete deployment config by provider
*/
async deleteByProvider(provider) {
const deployment = await this.readByProvider(provider);
return this.deleteOne(deployment.id);
}
/**
* Read deployment config with decrypted credentials (internal use)
*/
async readConfig(provider) {
const internalService = new ItemsService('directus_deployments', {
knex: this.knex,
schema: this.schema,
accountability: null,
});
const results = await internalService.readByQuery({
filter: { provider: { _eq: provider } },
limit: 1,
});
if (!results || results.length === 0) {
throw new Error(`Deployment config for "${provider}" not found`);
}
return results[0];
}
/**
* Parse JSON string or return value as-is
*/
parseValue(value, fallback) {
if (!value)
return fallback;
if (typeof value === 'string')
return parseJSON(value);
return value;
}
/**
* Get a deployment driver instance with decrypted credentials
*/
async getDriver(provider) {
const deployment = await this.readConfig(provider);
const credentials = this.parseValue(deployment.credentials, {});
const options = this.parseValue(deployment.options, {});
return getDeploymentDriver(deployment.provider, credentials, options);
}
/**
* List projects from provider with caching
*/
async listProviderProjects(provider) {
const cacheKey = `${provider}:projects`;
const { deploymentCache } = getCache();
// Check cache first
const cached = await getCacheValueWithTTL(deploymentCache, cacheKey);
if (cached) {
return { data: cached.data, remainingTTL: cached.remainingTTL };
}
// Fetch from driver
const driver = await this.getDriver(provider);
const projects = await driver.listProjects();
// Store in cache
await setCacheValueWithExpiry(deploymentCache, cacheKey, projects, DEPLOYMENT_CACHE_TTL);
// Return with full TTL (just cached)
return { data: projects, remainingTTL: DEPLOYMENT_CACHE_TTL };
}
/**
* Get project details from provider with caching
*/
async getProviderProject(provider, projectId) {
const cacheKey = `${provider}:project:${projectId}`;
const { deploymentCache } = getCache();
// Check cache first
const cached = await getCacheValueWithTTL(deploymentCache, cacheKey);
if (cached) {
return { data: cached.data, remainingTTL: cached.remainingTTL };
}
// Fetch from driver
const driver = await this.getDriver(provider);
const project = await driver.getProject(projectId);
// Store in cache
await setCacheValueWithExpiry(deploymentCache, cacheKey, project, DEPLOYMENT_CACHE_TTL);
// Return with full TTL (just cached)
return { data: project, remainingTTL: DEPLOYMENT_CACHE_TTL };
}
}