@directus/api
Advanced tools
| import type { Credentials, Deployment, Details, Log, Options, Project, TriggerResult } from '@directus/types'; | ||
| import { DeploymentDriver } from '../deployment.js'; | ||
| export interface NetlifyCredentials extends Credentials { | ||
| access_token: string; | ||
| } | ||
| export interface NetlifyOptions extends Options { | ||
| account_slug?: string; | ||
| } | ||
| export declare class NetlifyDriver extends DeploymentDriver<NetlifyCredentials, NetlifyOptions> { | ||
| private api; | ||
| constructor(credentials: NetlifyCredentials, options?: NetlifyOptions); | ||
| private handleApiError; | ||
| private mapStatus; | ||
| testConnection(): Promise<void>; | ||
| private mapSiteBase; | ||
| listProjects(): Promise<Project[]>; | ||
| getProject(projectId: string): Promise<Project>; | ||
| private mapDeployUrl; | ||
| listDeployments(projectId: string, limit?: number): Promise<Deployment[]>; | ||
| getDeployment(deploymentId: string): Promise<Details>; | ||
| triggerDeployment(projectId: string, options?: { | ||
| preview?: boolean; | ||
| clearCache?: boolean; | ||
| }): Promise<TriggerResult>; | ||
| cancelDeployment(deploymentId: string): Promise<void>; | ||
| private closeWsConnection; | ||
| private setupWsIdleTimeout; | ||
| private setupWsConnectionTimeout; | ||
| private getWsConnection; | ||
| getDeploymentLogs(deploymentId: string, options?: { | ||
| since?: Date; | ||
| }): Promise<Log[]>; | ||
| } |
| import { InvalidCredentialsError, ServiceUnavailableError } from '@directus/errors'; | ||
| import { NetlifyAPI } from '@netlify/api'; | ||
| import { isNumber } from 'lodash-es'; | ||
| import { DeploymentDriver } from '../deployment.js'; | ||
| const WS_CONNECTIONS = new Map(); | ||
| const WS_IDLE_TIMEOUT = 60_000; // 60 seconds | ||
| const WS_CONNECTION_TIMEOUT = 10_000; // 10 seconds | ||
| // eslint-disable-next-line no-control-regex | ||
| const ANSI_REGEX = /[\x1b]\[[0-9;]*m/g; | ||
| const WS_URL = 'wss://socketeer.services.netlify.com/build/logs'; | ||
| export class NetlifyDriver extends DeploymentDriver { | ||
| api; | ||
| constructor(credentials, options = {}) { | ||
| super(credentials, options); | ||
| this.api = new NetlifyAPI(this.credentials.access_token); | ||
| } | ||
| async handleApiError(cb) { | ||
| try { | ||
| return await cb(this.api); | ||
| } | ||
| catch (error) { | ||
| if (error instanceof Error && 'status' in error && isNumber(error.status) && error.status >= 400) { | ||
| if (error.status === 401 || error.status === 403) { | ||
| throw new InvalidCredentialsError(); | ||
| } | ||
| throw new ServiceUnavailableError({ service: 'netlify', reason: 'Netlify API error: ' + error.message }); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| mapStatus(netlifyState) { | ||
| const normalized = netlifyState?.toLowerCase(); | ||
| switch (normalized) { | ||
| case 'ready': | ||
| return 'ready'; | ||
| case 'error': | ||
| return 'error'; | ||
| case 'canceled': | ||
| return 'canceled'; | ||
| default: | ||
| return 'building'; | ||
| } | ||
| } | ||
| async testConnection() { | ||
| await this.handleApiError((api) => api.listSites({ per_page: 1 })); | ||
| } | ||
| mapSiteBase(site) { | ||
| const result = { | ||
| id: site.id, | ||
| name: site.name, | ||
| deployable: Boolean(site.build_settings?.provider && site.build_settings?.repo_url), | ||
| }; | ||
| // Use custom domain if available, otherwise ssl_url or url | ||
| if (site.custom_domain) { | ||
| result.url = `https://${site.custom_domain}`; | ||
| } | ||
| else if (site.ssl_url) { | ||
| result.url = site.ssl_url; | ||
| } | ||
| else if (site.url) { | ||
| result.url = site.url; | ||
| } | ||
| return result; | ||
| } | ||
| async listProjects() { | ||
| const params = { per_page: '100' }; | ||
| const response = await this.handleApiError((api) => { | ||
| return this.options.account_slug | ||
| ? api.listSitesForAccount({ | ||
| account_slug: this.options.account_slug, | ||
| ...params, | ||
| }) | ||
| : api.listSites(params); | ||
| }); | ||
| return response.map((site) => this.mapSiteBase(site)); | ||
| } | ||
| async getProject(projectId) { | ||
| const site = await this.handleApiError((api) => api.getSite({ siteId: projectId })); | ||
| const result = this.mapSiteBase(site); | ||
| // Add published deploy info if available | ||
| if (site.published_deploy) { | ||
| const deploy = site.published_deploy; | ||
| if (deploy.state && deploy.created_at) { | ||
| result.latest_deployment = { | ||
| status: this.mapStatus(deploy.state), | ||
| created_at: new Date(deploy.created_at), | ||
| ...(deploy.published_at && { finished_at: new Date(deploy.published_at) }), | ||
| }; | ||
| } | ||
| } | ||
| if (site.created_at) { | ||
| result.created_at = new Date(site.created_at); | ||
| } | ||
| if (site.updated_at) { | ||
| result.updated_at = new Date(site.updated_at); | ||
| } | ||
| return result; | ||
| } | ||
| mapDeployUrl(deploy) { | ||
| return deploy['ssl_url'] ?? deploy['deploy_ssl_url'] ?? deploy['deploy_url'] ?? deploy['url']; | ||
| } | ||
| async listDeployments(projectId, limit = 20) { | ||
| const response = await this.handleApiError((api) => api.listSiteDeploys({ site_id: projectId, per_page: limit })); | ||
| return response.map((deploy) => { | ||
| const result = { | ||
| id: deploy.id, | ||
| project_id: deploy.site_id, | ||
| status: this.mapStatus(deploy.state), | ||
| created_at: new Date(deploy.created_at), | ||
| }; | ||
| const url = this.mapDeployUrl(deploy); | ||
| if (url) | ||
| result.url = url; | ||
| if (deploy.published_at) { | ||
| result.finished_at = new Date(deploy.published_at); | ||
| } | ||
| if (deploy.error_message) { | ||
| result.error_message = deploy.error_message; | ||
| } | ||
| return result; | ||
| }); | ||
| } | ||
| async getDeployment(deploymentId) { | ||
| const deploy = await this.handleApiError((api) => api.getDeploy({ deployId: deploymentId })); | ||
| const result = { | ||
| id: deploy.id, | ||
| project_id: deploy.site_id, | ||
| status: this.mapStatus(deploy.state), | ||
| created_at: new Date(deploy.created_at), | ||
| }; | ||
| const url = this.mapDeployUrl(deploy); | ||
| if (url) | ||
| result.url = url; | ||
| if (deploy.published_at) { | ||
| result.finished_at = new Date(deploy.published_at); | ||
| } | ||
| if (deploy.error_message) { | ||
| result.error_message = deploy.error_message; | ||
| } | ||
| return result; | ||
| } | ||
| async triggerDeployment(projectId, options) { | ||
| // Netlify builds endpoint returns a Build object with deploy_id and deploy_state | ||
| const buildResponse = await this.handleApiError((api) => api.createSiteBuild({ | ||
| site_id: projectId, | ||
| clear_cache: options?.clearCache || false, | ||
| })); | ||
| const deployState = await this.handleApiError((api) => api.getDeploy({ deployId: buildResponse.deploy_id })); | ||
| const triggerResult = { | ||
| deployment_id: buildResponse.deploy_id, | ||
| status: this.mapStatus(deployState.state), | ||
| }; | ||
| return triggerResult; | ||
| } | ||
| async cancelDeployment(deploymentId) { | ||
| await this.handleApiError((api) => api.cancelSiteDeploy({ deployId: deploymentId })); | ||
| this.closeWsConnection(deploymentId); | ||
| } | ||
| closeWsConnection(deploymentId, remove = true) { | ||
| const connection = WS_CONNECTIONS.get(deploymentId); | ||
| if (!connection) | ||
| return; | ||
| connection.ws.close(); | ||
| if (remove) { | ||
| WS_CONNECTIONS.delete(deploymentId); | ||
| } | ||
| } | ||
| setupWsIdleTimeout(connection) { | ||
| if (connection.idleTimeout) { | ||
| clearTimeout(connection.idleTimeout); | ||
| } | ||
| connection.idleTimeout = setTimeout(() => { | ||
| this.closeWsConnection(connection.deploymentId); | ||
| }, WS_IDLE_TIMEOUT); | ||
| } | ||
| setupWsConnectionTimeout(connection, reject) { | ||
| if (connection.connectionTimeout) { | ||
| clearTimeout(connection.connectionTimeout); | ||
| } | ||
| connection.connectionTimeout = setTimeout(() => { | ||
| this.closeWsConnection(connection.deploymentId); | ||
| reject(new ServiceUnavailableError({ service: 'netlify', reason: 'WebSocket connection timeout' })); | ||
| }, WS_CONNECTION_TIMEOUT); | ||
| } | ||
| getWsConnection(deploymentId) { | ||
| return new Promise((resolve, reject) => { | ||
| const existingConnection = WS_CONNECTIONS.get(deploymentId); | ||
| if (existingConnection) { | ||
| this.setupWsIdleTimeout(existingConnection); | ||
| return resolve(existingConnection); | ||
| } | ||
| let resolveCompleted; | ||
| const completed = new Promise((res) => { | ||
| resolveCompleted = res; | ||
| }); | ||
| const connection = { | ||
| ws: new WebSocket(WS_URL), | ||
| logs: [], | ||
| deploymentId, | ||
| completed, | ||
| resolveCompleted: resolveCompleted, | ||
| }; | ||
| this.setupWsConnectionTimeout(connection, reject); | ||
| connection.ws.addEventListener('open', () => { | ||
| if (connection.connectionTimeout) { | ||
| clearTimeout(connection.connectionTimeout); | ||
| connection.connectionTimeout = undefined; | ||
| } | ||
| this.setupWsIdleTimeout(connection); | ||
| const payload = JSON.stringify({ | ||
| deploy_id: deploymentId, | ||
| access_token: this.credentials.access_token, | ||
| }); | ||
| connection.ws.send(payload); | ||
| resolve(connection); | ||
| WS_CONNECTIONS.set(deploymentId, connection); | ||
| }); | ||
| connection.ws.addEventListener('message', (event) => { | ||
| const data = JSON.parse(event.data); | ||
| const cleanMessage = data.message.replace(/\r/g, '').replace(ANSI_REGEX, ''); | ||
| let logType = 'stdout'; | ||
| if (data.type === 'report') { | ||
| logType = cleanMessage.includes('Failing build') ? 'stderr' : 'info'; | ||
| } | ||
| connection.logs.push({ | ||
| timestamp: new Date(data.ts), | ||
| type: logType, | ||
| message: cleanMessage, | ||
| }); | ||
| // If we receive a "report" type message, the build is complete. | ||
| // Close the WebSocket connection but don't yet remove the logs, allowing the client to fetch them until the idle timeout expires. | ||
| if (data.type === 'report') { | ||
| connection.resolveCompleted(); | ||
| this.closeWsConnection(deploymentId, false); | ||
| } | ||
| }); | ||
| connection.ws.addEventListener('error', () => { | ||
| this.closeWsConnection(deploymentId); | ||
| reject(new ServiceUnavailableError({ service: 'netlify', reason: 'WebSocket connection error' })); | ||
| }); | ||
| connection.ws.addEventListener('close', () => { | ||
| if (connection.connectionTimeout) { | ||
| clearTimeout(connection.connectionTimeout); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| async getDeploymentLogs(deploymentId, options) { | ||
| const deploy = await this.handleApiError((api) => api.getDeploy({ deployId: deploymentId })); | ||
| const connection = await this.getWsConnection(deploymentId); | ||
| // Build already finished — WS is replaying logs, wait for all of them | ||
| if (this.mapStatus(deploy.state) !== 'building') { | ||
| await connection.completed; | ||
| } | ||
| if (options?.since) { | ||
| return connection.logs.filter((log) => log.timestamp >= options.since); | ||
| } | ||
| return connection.logs; | ||
| } | ||
| } |
@@ -31,3 +31,4 @@ import { useEnv } from '@directus/env'; | ||
| res.setHeader('Content-Type', 'application/zip'); | ||
| res.setHeader('Content-Disposition', `attachment; filename="folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip"`); | ||
| const folderName = `folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip`; | ||
| res.setHeader('Content-Disposition', contentDisposition(folderName, { type: 'attachment' })); | ||
| archive.pipe(res); | ||
@@ -34,0 +35,0 @@ await complete(); |
@@ -34,4 +34,5 @@ import { applyFilter } from '../../run-ast/lib/apply-query/filter/index.js'; | ||
| } | ||
| return this.knex.raw('(' + countQuery.toQuery() + ')'); | ||
| const { sql, bindings } = countQuery.toSQL(); | ||
| return this.knex.raw(`(${sql})`, bindings); | ||
| } | ||
| } |
@@ -1,2 +0,2 @@ | ||
| import { VercelDriver } from './deployment/drivers/index.js'; | ||
| import { NetlifyDriver, VercelDriver } from './deployment/drivers/index.js'; | ||
| /** | ||
@@ -11,2 +11,3 @@ * Registry of deployment driver constructors | ||
| drivers.set('vercel', VercelDriver); | ||
| drivers.set('netlify', NetlifyDriver); | ||
| } | ||
@@ -13,0 +14,0 @@ /** |
@@ -0,1 +1,2 @@ | ||
| export * from './netlify.js'; | ||
| export * from './vercel.js'; |
@@ -0,1 +1,2 @@ | ||
| export * from './netlify.js'; | ||
| export * from './vercel.js'; |
@@ -14,4 +14,15 @@ import { clamp } from 'lodash-es'; | ||
| if ((transformationParams.width || transformationParams.height) && file.width && file.height) { | ||
| const toWidth = transformationParams.width ? Number(transformationParams.width) : undefined; | ||
| const toHeight = transformationParams.height ? Number(transformationParams.height) : undefined; | ||
| let toWidth = transformationParams.width ? Number(transformationParams.width) : undefined; | ||
| let toHeight = transformationParams.height ? Number(transformationParams.height) : undefined; | ||
| /* | ||
| * When withoutEnlargement is true, clamp target dimensions to original dimensions to prevent "bad extract area" errors when using focal points. | ||
| */ | ||
| if (transformationParams.withoutEnlargement) { | ||
| if (toWidth !== undefined) { | ||
| toWidth = Math.min(toWidth, file.width); | ||
| } | ||
| if (toHeight !== undefined) { | ||
| toHeight = Math.min(toHeight, file.height); | ||
| } | ||
| } | ||
| const toFocalPointX = transformationParams.focal_point_x | ||
@@ -18,0 +29,0 @@ ? Number(transformationParams.focal_point_x) |
+21
-20
| { | ||
| "name": "@directus/api", | ||
| "version": "33.1.1", | ||
| "version": "33.2.0", | ||
| "description": "Directus is a real-time API and App dashboard for managing SQL database content", | ||
@@ -69,2 +69,3 @@ "keywords": [ | ||
| "@modelcontextprotocol/sdk": "1.26.0", | ||
| "@netlify/api": "14.0.14", | ||
| "@rollup/plugin-alias": "5.1.1", | ||
@@ -165,26 +166,26 @@ "@rollup/plugin-node-resolve": "16.0.3", | ||
| "@directus/ai": "1.1.0", | ||
| "@directus/app": "15.1.1", | ||
| "@directus/constants": "14.0.0", | ||
| "@directus/env": "5.5.1", | ||
| "@directus/extensions": "3.0.17", | ||
| "@directus/env": "5.5.2", | ||
| "@directus/app": "15.2.0", | ||
| "@directus/errors": "2.2.0", | ||
| "@directus/extensions-sdk": "17.0.7", | ||
| "@directus/extensions-registry": "3.0.17", | ||
| "@directus/memory": "3.1.0", | ||
| "@directus/extensions": "3.0.18", | ||
| "@directus/extensions-registry": "3.0.18", | ||
| "@directus/extensions-sdk": "17.0.8", | ||
| "@directus/format-title": "12.1.1", | ||
| "@directus/pressure": "3.0.15", | ||
| "@directus/memory": "3.1.1", | ||
| "@directus/pressure": "3.0.16", | ||
| "@directus/schema": "13.0.5", | ||
| "@directus/schema-builder": "0.0.12", | ||
| "@directus/schema-builder": "0.0.13", | ||
| "@directus/specs": "12.0.0", | ||
| "@directus/storage": "12.0.3", | ||
| "@directus/storage-driver-azure": "12.0.15", | ||
| "@directus/storage-driver-cloudinary": "12.0.15", | ||
| "@directus/storage-driver-azure": "12.0.16", | ||
| "@directus/storage-driver-cloudinary": "12.0.16", | ||
| "@directus/storage-driver-local": "12.0.3", | ||
| "@directus/storage-driver-s3": "12.1.1", | ||
| "@directus/storage-driver-supabase": "3.0.15", | ||
| "@directus/storage-driver-gcs": "12.0.15", | ||
| "@directus/utils": "13.2.0", | ||
| "@directus/constants": "14.0.0", | ||
| "@directus/storage-driver-gcs": "12.0.16", | ||
| "@directus/storage-driver-supabase": "3.0.16", | ||
| "@directus/system-data": "4.1.0", | ||
| "@directus/validation": "2.0.15", | ||
| "directus": "11.15.1" | ||
| "@directus/utils": "13.2.1", | ||
| "@directus/storage-driver-s3": "12.1.2", | ||
| "@directus/validation": "2.0.16", | ||
| "directus": "11.15.2" | ||
| }, | ||
@@ -232,4 +233,4 @@ "devDependencies": { | ||
| "vitest": "3.2.4", | ||
| "@directus/schema-builder": "0.0.12", | ||
| "@directus/types": "14.1.0" | ||
| "@directus/schema-builder": "0.0.13", | ||
| "@directus/types": "14.2.0" | ||
| }, | ||
@@ -236,0 +237,0 @@ "optionalDependencies": { |
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2573431
0.49%1417
0.14%58318
0.53%135
0.75%158
0.64%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated