UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

203 lines (202 loc) 7.74 kB
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 }; } }