@directus/api 33.0.0 → 33.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/chat/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +127 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/items/index.js +4 -1
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/schema.js +1 -1
- package/dist/app.js +8 -1
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +142 -137
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +25 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/controllers/deployment.d.ts +2 -0
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/fields.js +6 -4
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/middleware/respond.js +27 -14
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
- package/dist/server.js +2 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/server.js +1 -0
- package/dist/services/specifications.js +2 -2
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-service.js +7 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/sanitize-query.js +7 -2
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/package.json +32 -30
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Credentials, Deployment, Details, Log, Options, Project, TriggerResult } from '@directus/types';
|
|
2
|
+
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
3
|
+
export type DeploymentRequestOptions = Pick<AxiosRequestConfig<string>, 'method' | 'headers'> & {
|
|
4
|
+
body?: string | null;
|
|
5
|
+
params?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export declare abstract class DeploymentDriver<TCredentials extends Credentials = Credentials, TOptions extends Options = Options> {
|
|
8
|
+
credentials: TCredentials;
|
|
9
|
+
options: TOptions;
|
|
10
|
+
constructor(credentials: TCredentials, options?: TOptions);
|
|
11
|
+
protected axiosRequest<T>(apiUrl: string, endpoint: string, options?: DeploymentRequestOptions): Promise<AxiosResponse<T>>;
|
|
12
|
+
/**
|
|
13
|
+
* Test connection with provider using credentials
|
|
14
|
+
*
|
|
15
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
16
|
+
*/
|
|
17
|
+
abstract testConnection(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* List all available projects from provider
|
|
20
|
+
*
|
|
21
|
+
* @returns Array of external projects
|
|
22
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
23
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
24
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
25
|
+
*/
|
|
26
|
+
abstract listProjects(): Promise<Project[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Get project details by ID
|
|
29
|
+
*
|
|
30
|
+
* @param projectId External project ID
|
|
31
|
+
* @returns Project details
|
|
32
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
33
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
34
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
35
|
+
*/
|
|
36
|
+
abstract getProject(projectId: string): Promise<Project>;
|
|
37
|
+
/**
|
|
38
|
+
* List deployments for a project
|
|
39
|
+
*
|
|
40
|
+
* @param projectId External project ID
|
|
41
|
+
* @param limit Number of deployments to return
|
|
42
|
+
* @returns Array of deployments
|
|
43
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
44
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
45
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
46
|
+
*/
|
|
47
|
+
abstract listDeployments(projectId: string, limit?: number): Promise<Deployment[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Get deployment details including logs
|
|
50
|
+
*
|
|
51
|
+
* @param deploymentId External deployment ID
|
|
52
|
+
* @returns Deployment details with logs
|
|
53
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
54
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
55
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
56
|
+
*/
|
|
57
|
+
abstract getDeployment(deploymentId: string): Promise<Details>;
|
|
58
|
+
/**
|
|
59
|
+
* Trigger a new deployment
|
|
60
|
+
*
|
|
61
|
+
* @param projectId External project ID
|
|
62
|
+
* @param options Deployment options
|
|
63
|
+
* @returns Deployment result
|
|
64
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
65
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
66
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
67
|
+
*/
|
|
68
|
+
abstract triggerDeployment(projectId: string, options?: {
|
|
69
|
+
preview?: boolean;
|
|
70
|
+
clearCache?: boolean;
|
|
71
|
+
}): Promise<TriggerResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Cancel a running deployment
|
|
74
|
+
*
|
|
75
|
+
* @param deploymentId External deployment ID
|
|
76
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
77
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
78
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
79
|
+
*/
|
|
80
|
+
abstract cancelDeployment(deploymentId: string): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Get deployment build logs
|
|
83
|
+
*
|
|
84
|
+
* @param deploymentId External deployment ID
|
|
85
|
+
* @param options.since Only return logs after this timestamp
|
|
86
|
+
* @returns Array of log entries
|
|
87
|
+
* @throws {InvalidCredentialsError} When API credentials are invalid
|
|
88
|
+
* @throws {HitRateLimitError} When rate limit is exceeded
|
|
89
|
+
* @throws {ServiceUnavailableError} When provider API fails
|
|
90
|
+
*/
|
|
91
|
+
abstract getDeploymentLogs(deploymentId: string, options?: {
|
|
92
|
+
since?: Date;
|
|
93
|
+
}): Promise<Log[]>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getAxios } from '../request/index.js';
|
|
2
|
+
export class DeploymentDriver {
|
|
3
|
+
credentials;
|
|
4
|
+
options;
|
|
5
|
+
constructor(credentials, options = {}) {
|
|
6
|
+
this.credentials = credentials;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
async axiosRequest(apiUrl, endpoint, options = {}) {
|
|
10
|
+
const { params, ...requestOptions } = options;
|
|
11
|
+
const url = new URL(endpoint, apiUrl);
|
|
12
|
+
if (params) {
|
|
13
|
+
for (const [key, value] of Object.entries(params)) {
|
|
14
|
+
url.searchParams.set(key, value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const axios = await getAxios();
|
|
18
|
+
const requestConfig = {
|
|
19
|
+
url: url.toString(),
|
|
20
|
+
method: requestOptions.method ?? 'GET',
|
|
21
|
+
validateStatus: () => true,
|
|
22
|
+
headers: requestOptions.headers ?? {},
|
|
23
|
+
};
|
|
24
|
+
if (requestOptions.body) {
|
|
25
|
+
requestConfig.data = requestOptions.body;
|
|
26
|
+
}
|
|
27
|
+
return await axios.request(requestConfig);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vercel.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vercel.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Credentials, Deployment, Details, Log, Options, Project, TriggerResult } from '@directus/types';
|
|
2
|
+
import { DeploymentDriver } from '../deployment.js';
|
|
3
|
+
export interface VercelCredentials extends Credentials {
|
|
4
|
+
access_token: string;
|
|
5
|
+
}
|
|
6
|
+
export interface VercelOptions extends Options {
|
|
7
|
+
team_id?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class VercelDriver extends DeploymentDriver<VercelCredentials, VercelOptions> {
|
|
10
|
+
private static readonly API_URL;
|
|
11
|
+
private requestLimit;
|
|
12
|
+
constructor(credentials: VercelCredentials, options?: VercelOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Make authenticated request with retry on rate limit and concurrency control
|
|
15
|
+
*/
|
|
16
|
+
private request;
|
|
17
|
+
private mapStatus;
|
|
18
|
+
testConnection(): Promise<void>;
|
|
19
|
+
private mapProjectBase;
|
|
20
|
+
listProjects(): Promise<Project[]>;
|
|
21
|
+
getProject(projectId: string): Promise<Project>;
|
|
22
|
+
listDeployments(projectId: string, limit?: number): Promise<Deployment[]>;
|
|
23
|
+
getDeployment(deploymentId: string): Promise<Details>;
|
|
24
|
+
triggerDeployment(projectId: string, options?: {
|
|
25
|
+
preview?: boolean;
|
|
26
|
+
clearCache?: boolean;
|
|
27
|
+
}): Promise<TriggerResult>;
|
|
28
|
+
cancelDeployment(deploymentId: string): Promise<void>;
|
|
29
|
+
getDeploymentLogs(deploymentId: string, options?: {
|
|
30
|
+
since?: Date;
|
|
31
|
+
}): Promise<Log[]>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { HitRateLimitError, InvalidCredentialsError, ServiceUnavailableError } from '@directus/errors';
|
|
2
|
+
import pLimit from 'p-limit';
|
|
3
|
+
import { DeploymentDriver } from '../deployment.js';
|
|
4
|
+
export class VercelDriver extends DeploymentDriver {
|
|
5
|
+
static API_URL = 'https://api.vercel.com';
|
|
6
|
+
requestLimit = pLimit(5);
|
|
7
|
+
constructor(credentials, options = {}) {
|
|
8
|
+
super(credentials, options);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Make authenticated request with retry on rate limit and concurrency control
|
|
12
|
+
*/
|
|
13
|
+
async request(endpoint, options = {}, retryCount = 0) {
|
|
14
|
+
return this.requestLimit(async () => {
|
|
15
|
+
const response = await this.axiosRequest(VercelDriver.API_URL, endpoint, {
|
|
16
|
+
...options,
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${this.credentials.access_token}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
...(options.headers ?? {}),
|
|
21
|
+
},
|
|
22
|
+
params: {
|
|
23
|
+
...(this.options.team_id ? { teamId: this.options.team_id } : {}),
|
|
24
|
+
...(options.params ?? {}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Handle rate limiting with retry (max 3 retries)
|
|
28
|
+
if (response.status === 429) {
|
|
29
|
+
const resetAt = parseInt(response.headers['x-ratelimit-reset'] || '0');
|
|
30
|
+
const limit = parseInt(response.headers['x-ratelimit-limit'] || '0');
|
|
31
|
+
if (retryCount < 3) {
|
|
32
|
+
const waitTime = resetAt > 0 ? Math.max(resetAt * 1000 - Date.now(), 1000) : 1000 * (retryCount + 1);
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
34
|
+
return this.request(endpoint, options, retryCount + 1);
|
|
35
|
+
}
|
|
36
|
+
// Max retries exceeded
|
|
37
|
+
throw new HitRateLimitError({
|
|
38
|
+
limit,
|
|
39
|
+
reset: new Date(resetAt > 0 ? resetAt * 1000 : Date.now()),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const body = response.data;
|
|
43
|
+
if (response.status >= 400) {
|
|
44
|
+
const message = typeof body === 'object' && body !== null && 'error' in body
|
|
45
|
+
? body.error?.message || `Vercel API error: ${response.status}`
|
|
46
|
+
: `Vercel API error: ${response.status}`;
|
|
47
|
+
if (response.status === 401 || response.status === 403) {
|
|
48
|
+
throw new InvalidCredentialsError();
|
|
49
|
+
}
|
|
50
|
+
throw new ServiceUnavailableError({ service: 'vercel', reason: message });
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
mapStatus(vercelStatus) {
|
|
56
|
+
const normalized = vercelStatus?.toLowerCase();
|
|
57
|
+
switch (normalized) {
|
|
58
|
+
case 'building':
|
|
59
|
+
case 'error':
|
|
60
|
+
case 'canceled':
|
|
61
|
+
case 'ready':
|
|
62
|
+
return normalized;
|
|
63
|
+
case 'queued':
|
|
64
|
+
case 'initializing':
|
|
65
|
+
case 'analyzing':
|
|
66
|
+
case 'deploying':
|
|
67
|
+
default:
|
|
68
|
+
return 'building';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async testConnection() {
|
|
72
|
+
return await this.request('/v9/projects', { params: { limit: '1' } });
|
|
73
|
+
}
|
|
74
|
+
mapProjectBase(project) {
|
|
75
|
+
const result = {
|
|
76
|
+
id: project.id,
|
|
77
|
+
name: project.name,
|
|
78
|
+
deployable: Boolean(project.link?.type),
|
|
79
|
+
};
|
|
80
|
+
if (project.framework) {
|
|
81
|
+
result.framework = project.framework;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
async listProjects() {
|
|
86
|
+
const response = await this.request('/v9/projects');
|
|
87
|
+
return response.projects.map((project) => this.mapProjectBase(project));
|
|
88
|
+
}
|
|
89
|
+
async getProject(projectId) {
|
|
90
|
+
const project = await this.request(`/v9/projects/${projectId}`);
|
|
91
|
+
const result = this.mapProjectBase(project);
|
|
92
|
+
const production = project.targets?.production;
|
|
93
|
+
if (production?.alias?.[0]) {
|
|
94
|
+
result.url = `https://${production.alias[0]}`;
|
|
95
|
+
}
|
|
96
|
+
// Latest deployment info from detail endpoint
|
|
97
|
+
if (production?.readyState && production.createdAt) {
|
|
98
|
+
result.latest_deployment = {
|
|
99
|
+
status: this.mapStatus(production.readyState),
|
|
100
|
+
created_at: new Date(production.createdAt),
|
|
101
|
+
...(production.readyAt && { finished_at: new Date(production.readyAt) }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (project.createdAt) {
|
|
105
|
+
result.created_at = new Date(project.createdAt);
|
|
106
|
+
}
|
|
107
|
+
if (project.updatedAt) {
|
|
108
|
+
result.updated_at = new Date(project.updatedAt);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
async listDeployments(projectId, limit = 20) {
|
|
113
|
+
const url = `/v6/deployments?projectId=${encodeURIComponent(projectId)}&limit=${limit}`;
|
|
114
|
+
const response = await this.request(url);
|
|
115
|
+
return response.deployments.map((deployment) => {
|
|
116
|
+
const result = {
|
|
117
|
+
id: deployment.uid,
|
|
118
|
+
project_id: deployment.projectId ?? projectId,
|
|
119
|
+
status: this.mapStatus(deployment.state),
|
|
120
|
+
created_at: new Date(deployment.createdAt),
|
|
121
|
+
};
|
|
122
|
+
if (deployment.url) {
|
|
123
|
+
result.url = `https://${deployment.url}`;
|
|
124
|
+
}
|
|
125
|
+
if (deployment.ready) {
|
|
126
|
+
result.finished_at = new Date(deployment.ready);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async getDeployment(deploymentId) {
|
|
132
|
+
const deployment = await this.request(`/v13/deployments/${encodeURIComponent(deploymentId)}`);
|
|
133
|
+
const result = {
|
|
134
|
+
id: deployment.id,
|
|
135
|
+
project_id: deployment.projectId ?? '',
|
|
136
|
+
status: this.mapStatus(deployment.status || deployment.state),
|
|
137
|
+
created_at: new Date(deployment.createdAt),
|
|
138
|
+
};
|
|
139
|
+
if (deployment.url) {
|
|
140
|
+
result.url = `https://${deployment.url}`;
|
|
141
|
+
}
|
|
142
|
+
if (deployment.ready) {
|
|
143
|
+
result.finished_at = new Date(deployment.ready);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
async triggerDeployment(projectId, options) {
|
|
148
|
+
// Fetch project to get realtime required name needed for the vercel request
|
|
149
|
+
const project = await this.request(`/v9/projects/${projectId}`);
|
|
150
|
+
const body = {
|
|
151
|
+
name: project.name,
|
|
152
|
+
project: projectId,
|
|
153
|
+
};
|
|
154
|
+
if (!options?.preview) {
|
|
155
|
+
body['target'] = 'production';
|
|
156
|
+
}
|
|
157
|
+
// Add required gitSource
|
|
158
|
+
if (project.link?.type) {
|
|
159
|
+
body['gitSource'] = {
|
|
160
|
+
type: project.link.type,
|
|
161
|
+
ref: project.link.productionBranch,
|
|
162
|
+
repoId: project.link.repoId,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// forceNew=1 skips build cache when clearCache is true
|
|
166
|
+
const response = await this.request('/v13/deployments', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
...(options?.clearCache && { params: { forceNew: '1' } }),
|
|
170
|
+
});
|
|
171
|
+
const triggerResult = {
|
|
172
|
+
deployment_id: response.id,
|
|
173
|
+
status: this.mapStatus(response.status),
|
|
174
|
+
};
|
|
175
|
+
if (response.url) {
|
|
176
|
+
triggerResult.url = `https://${response.url}`;
|
|
177
|
+
}
|
|
178
|
+
return triggerResult;
|
|
179
|
+
}
|
|
180
|
+
async cancelDeployment(deploymentId) {
|
|
181
|
+
await this.request(`/v12/deployments/${encodeURIComponent(deploymentId)}/cancel`, {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async getDeploymentLogs(deploymentId, options) {
|
|
186
|
+
let url = `/v3/deployments/${encodeURIComponent(deploymentId)}/events`;
|
|
187
|
+
// Vercel's since parameter uses milliseconds timestamp
|
|
188
|
+
if (options?.since) {
|
|
189
|
+
const sinceMs = options.since.getTime();
|
|
190
|
+
url += `?since=${sinceMs}`;
|
|
191
|
+
}
|
|
192
|
+
const response = await this.request(url);
|
|
193
|
+
const mapEventType = (type) => {
|
|
194
|
+
if (type === 'stderr')
|
|
195
|
+
return 'stderr';
|
|
196
|
+
if (type === 'command')
|
|
197
|
+
return 'info';
|
|
198
|
+
return 'stdout';
|
|
199
|
+
};
|
|
200
|
+
return response
|
|
201
|
+
.filter((event) => event.type === 'stdout' || event.type === 'stderr' || event.type === 'command')
|
|
202
|
+
.map((event) => ({
|
|
203
|
+
timestamp: new Date(event.created),
|
|
204
|
+
type: mapEventType(event.type),
|
|
205
|
+
message: event.text || event.payload?.text || '',
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Credentials, Options, ProviderType } from '@directus/types';
|
|
2
|
+
import type { DeploymentDriver } from './deployment/deployment.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register all deployment drivers
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerDeploymentDrivers(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Get a deployment driver instance
|
|
9
|
+
*
|
|
10
|
+
* @param provider Provider name (vercel, netlify, aws, etc.)
|
|
11
|
+
* @param credentials Provider credentials (decrypted from DB)
|
|
12
|
+
* @param options Additional provider options
|
|
13
|
+
* @returns Deployment driver instance
|
|
14
|
+
* @throws Error if provider is not supported
|
|
15
|
+
*/
|
|
16
|
+
export declare function getDeploymentDriver(provider: ProviderType, credentials: Credentials, options?: Options): DeploymentDriver;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a provider is supported
|
|
19
|
+
*/
|
|
20
|
+
export declare function isValidProviderType(provider: string): provider is ProviderType;
|
|
21
|
+
/**
|
|
22
|
+
* Get list of supported provider types
|
|
23
|
+
*/
|
|
24
|
+
export declare function getSupportedProviderTypes(): ProviderType[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { VercelDriver } from './deployment/drivers/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registry of deployment driver constructors
|
|
4
|
+
*/
|
|
5
|
+
const drivers = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register all deployment drivers
|
|
8
|
+
*/
|
|
9
|
+
export function registerDeploymentDrivers() {
|
|
10
|
+
drivers.set('vercel', VercelDriver);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get a deployment driver instance
|
|
14
|
+
*
|
|
15
|
+
* @param provider Provider name (vercel, netlify, aws, etc.)
|
|
16
|
+
* @param credentials Provider credentials (decrypted from DB)
|
|
17
|
+
* @param options Additional provider options
|
|
18
|
+
* @returns Deployment driver instance
|
|
19
|
+
* @throws Error if provider is not supported
|
|
20
|
+
*/
|
|
21
|
+
export function getDeploymentDriver(provider, credentials, options) {
|
|
22
|
+
const Driver = drivers.get(provider);
|
|
23
|
+
if (!Driver) {
|
|
24
|
+
throw new Error(`Deployment driver "${provider}" is not supported`);
|
|
25
|
+
}
|
|
26
|
+
return new Driver(credentials, options);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a provider is supported
|
|
30
|
+
*/
|
|
31
|
+
export function isValidProviderType(provider) {
|
|
32
|
+
return drivers.has(provider);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get list of supported provider types
|
|
36
|
+
*/
|
|
37
|
+
export function getSupportedProviderTypes() {
|
|
38
|
+
return Array.from(drivers.keys());
|
|
39
|
+
}
|
|
@@ -15,6 +15,10 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
15
15
|
const env = useEnv();
|
|
16
16
|
const logger = useLogger();
|
|
17
17
|
const { cache } = getCache();
|
|
18
|
+
// Support custom cache instance and TTL via res.locals
|
|
19
|
+
const cacheInstance = res.locals['cacheInstance'] || cache;
|
|
20
|
+
const cacheTTL = res.locals['cacheTTL'] ?? getMilliseconds(env['CACHE_TTL']);
|
|
21
|
+
const hasCustomCache = !!res.locals['cacheInstance'];
|
|
18
22
|
let exceedsMaxSize = false;
|
|
19
23
|
if (env['CACHE_VALUE_MAX_SIZE'] !== false) {
|
|
20
24
|
const valueSize = res.locals['payload'] ? stringByteSize(JSON.stringify(res.locals['payload'])) : 0;
|
|
@@ -22,26 +26,35 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
22
26
|
if (maxSize !== null)
|
|
23
27
|
exceedsMaxSize = valueSize > maxSize;
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
// Custom cache bypasses global cache settings (CACHE_ENABLED, permissionsCacheable)
|
|
30
|
+
const shouldCache = hasCustomCache
|
|
31
|
+
? res.locals['cache'] !== false && cacheInstance && !req.sanitizedQuery.export && exceedsMaxSize === false
|
|
32
|
+
: (req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
|
|
33
|
+
req.originalUrl?.startsWith('/auth') === false &&
|
|
34
|
+
env['CACHE_ENABLED'] === true &&
|
|
35
|
+
cache &&
|
|
36
|
+
!req.sanitizedQuery.export &&
|
|
37
|
+
res.locals['cache'] !== false &&
|
|
38
|
+
exceedsMaxSize === false &&
|
|
39
|
+
(await permissionsCacheable(req.collection, {
|
|
40
|
+
knex: getDatabase(),
|
|
41
|
+
schema: req.schema,
|
|
42
|
+
}, req.accountability));
|
|
43
|
+
if (shouldCache) {
|
|
36
44
|
const key = await getCacheKey(req);
|
|
37
45
|
try {
|
|
38
|
-
await setCacheValue(
|
|
39
|
-
await setCacheValue(
|
|
46
|
+
await setCacheValue(cacheInstance, key, res.locals['payload'], cacheTTL);
|
|
47
|
+
await setCacheValue(cacheInstance, `${key}__expires_at`, { exp: Date.now() + cacheTTL });
|
|
40
48
|
}
|
|
41
49
|
catch (err) {
|
|
42
50
|
logger.warn(err, `[cache] Couldn't set key ${key}. ${err}`);
|
|
43
51
|
}
|
|
44
|
-
res.setHeader('Cache-Control', getCacheControlHeader(req,
|
|
52
|
+
res.setHeader('Cache-Control', getCacheControlHeader(req, cacheTTL, !hasCustomCache, true));
|
|
53
|
+
res.setHeader('Vary', 'Origin, Cache-Control');
|
|
54
|
+
}
|
|
55
|
+
else if (res.locals['cacheTTL'] !== undefined) {
|
|
56
|
+
// Custom TTL for headers only (no storage) - useful when caching is handled elsewhere
|
|
57
|
+
res.setHeader('Cache-Control', getCacheControlHeader(req, cacheTTL, false, true));
|
|
45
58
|
res.setHeader('Vary', 'Origin, Cache-Control');
|
|
46
59
|
}
|
|
47
60
|
else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRelationInfo } from '
|
|
1
|
+
import { getRelationInfo } from '@directus/utils';
|
|
2
2
|
export function findRelatedCollection(collection, field, schema) {
|
|
3
3
|
const { relation } = getRelationInfo(schema.relations, collection, field);
|
|
4
4
|
if (!relation)
|
|
@@ -6,16 +6,23 @@ import { fetchAllowedFields } from '../../fetch-allowed-fields/fetch-allowed-fie
|
|
|
6
6
|
import { injectCases } from '../../process-ast/lib/inject-cases.js';
|
|
7
7
|
import { processAst } from '../../process-ast/process-ast.js';
|
|
8
8
|
export async function validateItemAccess(options, context) {
|
|
9
|
-
const
|
|
9
|
+
const collectionInfo = context.schema.collections[options.collection];
|
|
10
|
+
const primaryKeyField = collectionInfo?.primary;
|
|
10
11
|
if (!primaryKeyField) {
|
|
11
12
|
throw new Error(`Cannot find primary key for collection "${options.collection}"`);
|
|
12
13
|
}
|
|
14
|
+
const isSingleton = collectionInfo?.singleton === true;
|
|
15
|
+
const hasPrimaryKeys = options.primaryKeys && options.primaryKeys.length > 0;
|
|
16
|
+
// For non-singletons, we must have PKs to validate against
|
|
17
|
+
if (!isSingleton && !hasPrimaryKeys) {
|
|
18
|
+
throw new Error(`Primary keys are required for non-singleton collection "${options.collection}"`);
|
|
19
|
+
}
|
|
13
20
|
// When we're looking up access to specific items, we have to read them from the database to
|
|
14
21
|
// make sure you are allowed to access them.
|
|
15
22
|
const ast = {
|
|
16
23
|
type: 'root',
|
|
17
24
|
name: options.collection,
|
|
18
|
-
query: { limit: options.primaryKeys.length },
|
|
25
|
+
query: { limit: isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length },
|
|
19
26
|
// Act as if every field was a "normal" field
|
|
20
27
|
children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [], alias: false })) ??
|
|
21
28
|
[],
|
|
@@ -23,11 +30,14 @@ export async function validateItemAccess(options, context) {
|
|
|
23
30
|
};
|
|
24
31
|
await processAst({ ast, ...options }, context);
|
|
25
32
|
// Inject the filter after the permissions have been processed, as to not require access to the primary key
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// Skip adding filter for singletons without explicit PKs
|
|
34
|
+
if (hasPrimaryKeys) {
|
|
35
|
+
ast.query.filter = {
|
|
36
|
+
[primaryKeyField]: {
|
|
37
|
+
_in: options.primaryKeys,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
31
41
|
let hasItemRules;
|
|
32
42
|
let permissionedFields;
|
|
33
43
|
// Inject the root fields after the permissions have been processed, as to not require access to all collection fields
|
|
@@ -58,7 +68,8 @@ export async function validateItemAccess(options, context) {
|
|
|
58
68
|
knex: context.knex,
|
|
59
69
|
action: options.action,
|
|
60
70
|
});
|
|
61
|
-
const
|
|
71
|
+
const expectedCount = isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length;
|
|
72
|
+
const hasAccess = items && items.length === expectedCount;
|
|
62
73
|
if (!hasAccess) {
|
|
63
74
|
if (options.returnAllowedRootFields) {
|
|
64
75
|
return { accessAllowed: false, allowedRootFields: [] };
|
package/dist/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getAddress } from './utils/get-address.js';
|
|
|
15
15
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
16
16
|
import { getIPFromReq } from './utils/get-ip-from-req.js';
|
|
17
17
|
import { createLogsController, createSubscriptionController, createWebSocketController, getLogsController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
|
|
18
|
-
import { startWebSocketHandlers } from './websocket/handlers/index.js';
|
|
18
|
+
import { getCollabHandler, startWebSocketHandlers } from './websocket/handlers/index.js';
|
|
19
19
|
export let SERVER_ONLINE = true;
|
|
20
20
|
const env = useEnv();
|
|
21
21
|
const logger = useLogger();
|
|
@@ -101,6 +101,7 @@ export async function createServer() {
|
|
|
101
101
|
getSubscriptionController()?.terminate();
|
|
102
102
|
getWebSocketController()?.terminate();
|
|
103
103
|
getLogsController()?.terminate();
|
|
104
|
+
await getCollabHandler()?.terminate();
|
|
104
105
|
const database = getDatabase();
|
|
105
106
|
await database.destroy();
|
|
106
107
|
logger.info('Database connections destroyed');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AbstractServiceOptions, PrimaryKey } from '@directus/types';
|
|
2
|
+
import { ItemsService } from './items.js';
|
|
3
|
+
export interface DeploymentProject {
|
|
4
|
+
id: string;
|
|
5
|
+
deployment: string;
|
|
6
|
+
external_id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
date_created: string;
|
|
9
|
+
user_created: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class DeploymentProjectsService extends ItemsService<DeploymentProject> {
|
|
12
|
+
constructor(options: AbstractServiceOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Update project selection (create/delete)
|
|
15
|
+
*/
|
|
16
|
+
updateSelection(deploymentId: string, create: {
|
|
17
|
+
external_id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}[], deleteIds: PrimaryKey[]): Promise<DeploymentProject[]>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import getDatabase from '../database/index.js';
|
|
2
|
+
import { transaction } from '../utils/transaction.js';
|
|
3
|
+
import { ItemsService } from './items.js';
|
|
4
|
+
export class DeploymentProjectsService extends ItemsService {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super('directus_deployment_projects', options);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Update project selection (create/delete)
|
|
10
|
+
*/
|
|
11
|
+
async updateSelection(deploymentId, create, deleteIds) {
|
|
12
|
+
const db = getDatabase();
|
|
13
|
+
return transaction(db, async (trx) => {
|
|
14
|
+
const trxService = new DeploymentProjectsService({
|
|
15
|
+
accountability: this.accountability,
|
|
16
|
+
schema: this.schema,
|
|
17
|
+
knex: trx,
|
|
18
|
+
});
|
|
19
|
+
if (deleteIds.length > 0) {
|
|
20
|
+
await trxService.deleteMany(deleteIds);
|
|
21
|
+
}
|
|
22
|
+
if (create.length > 0) {
|
|
23
|
+
await trxService.createMany(create.map((p) => ({
|
|
24
|
+
deployment: deploymentId,
|
|
25
|
+
external_id: p.external_id,
|
|
26
|
+
name: p.name,
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
return trxService.readByQuery({
|
|
30
|
+
filter: { deployment: { _eq: deploymentId } },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AbstractServiceOptions } from '@directus/types';
|
|
2
|
+
import { ItemsService } from './items.js';
|
|
3
|
+
export interface DeploymentRun {
|
|
4
|
+
id: string;
|
|
5
|
+
project: string;
|
|
6
|
+
external_id: string;
|
|
7
|
+
target: string;
|
|
8
|
+
date_created: string;
|
|
9
|
+
user_created: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class DeploymentRunsService extends ItemsService<DeploymentRun> {
|
|
12
|
+
constructor(options: AbstractServiceOptions);
|
|
13
|
+
}
|