@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.
Files changed (117) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +127 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/items/index.js +4 -1
  22. package/dist/ai/tools/items/prompt.md +7 -9
  23. package/dist/ai/tools/schema.js +1 -1
  24. package/dist/app.js +8 -1
  25. package/dist/auth/drivers/ldap.d.ts +1 -1
  26. package/dist/auth/drivers/ldap.js +142 -137
  27. package/dist/cache.d.ts +12 -0
  28. package/dist/cache.js +25 -1
  29. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  30. package/dist/controllers/deployment.d.ts +2 -0
  31. package/dist/controllers/deployment.js +481 -0
  32. package/dist/controllers/fields.js +6 -4
  33. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  34. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  35. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  36. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  37. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  38. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  39. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  40. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  41. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  43. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  44. package/dist/deployment/deployment.d.ts +94 -0
  45. package/dist/deployment/deployment.js +29 -0
  46. package/dist/deployment/drivers/index.d.ts +1 -0
  47. package/dist/deployment/drivers/index.js +1 -0
  48. package/dist/deployment/drivers/vercel.d.ts +32 -0
  49. package/dist/deployment/drivers/vercel.js +208 -0
  50. package/dist/deployment/index.d.ts +2 -0
  51. package/dist/deployment/index.js +2 -0
  52. package/dist/deployment.d.ts +24 -0
  53. package/dist/deployment.js +39 -0
  54. package/dist/middleware/respond.js +27 -14
  55. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  56. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
  57. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
  58. package/dist/server.js +2 -1
  59. package/dist/services/deployment-projects.d.ts +20 -0
  60. package/dist/services/deployment-projects.js +34 -0
  61. package/dist/services/deployment-runs.d.ts +13 -0
  62. package/dist/services/deployment-runs.js +6 -0
  63. package/dist/services/deployment.d.ts +40 -0
  64. package/dist/services/deployment.js +202 -0
  65. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  66. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  67. package/dist/services/index.d.ts +3 -0
  68. package/dist/services/index.js +3 -0
  69. package/dist/services/server.js +1 -0
  70. package/dist/services/specifications.js +2 -2
  71. package/dist/services/versions.js +1 -1
  72. package/dist/telemetry/lib/get-report.js +2 -0
  73. package/dist/telemetry/types/report.d.ts +8 -0
  74. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  75. package/dist/telemetry/utils/get-settings.js +5 -0
  76. package/dist/utils/deep-map-response.d.ts +1 -1
  77. package/dist/utils/deep-map-response.js +1 -1
  78. package/dist/utils/get-column-path.js +1 -1
  79. package/dist/utils/get-service.js +7 -1
  80. package/dist/utils/is-field-allowed.d.ts +4 -0
  81. package/dist/utils/is-field-allowed.js +9 -0
  82. package/dist/utils/sanitize-query.js +7 -2
  83. package/dist/utils/versioning/handle-version.js +1 -1
  84. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  85. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  86. package/dist/websocket/collab/collab.d.ts +63 -0
  87. package/dist/websocket/collab/collab.js +481 -0
  88. package/dist/websocket/collab/constants.d.ts +1 -0
  89. package/dist/websocket/collab/constants.js +13 -0
  90. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  91. package/dist/websocket/collab/filter-to-fields.js +11 -0
  92. package/dist/websocket/collab/messenger.d.ts +43 -0
  93. package/dist/websocket/collab/messenger.js +225 -0
  94. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  95. package/dist/websocket/collab/payload-permissions.js +158 -0
  96. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  97. package/dist/websocket/collab/permissions-cache.js +204 -0
  98. package/dist/websocket/collab/room.d.ts +125 -0
  99. package/dist/websocket/collab/room.js +593 -0
  100. package/dist/websocket/collab/store.d.ts +7 -0
  101. package/dist/websocket/collab/store.js +33 -0
  102. package/dist/websocket/collab/types.d.ts +21 -0
  103. package/dist/websocket/collab/types.js +1 -0
  104. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  105. package/dist/websocket/collab/verify-permissions.js +100 -0
  106. package/dist/websocket/handlers/index.d.ts +2 -0
  107. package/dist/websocket/handlers/index.js +9 -0
  108. package/dist/websocket/utils/items.d.ts +2 -2
  109. package/dist/websocket/utils/message.d.ts +1 -1
  110. package/dist/websocket/utils/message.js +2 -2
  111. package/package.json +32 -30
  112. package/dist/utils/get-relation-info.d.ts +0 -6
  113. package/dist/utils/get-relation-info.js +0 -43
  114. package/dist/utils/get-relation-type.d.ts +0 -6
  115. package/dist/utils/get-relation-type.js +0 -18
  116. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  117. 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,2 @@
1
+ export * from './deployment.js';
2
+ export * from './drivers/index.js';
@@ -0,0 +1,2 @@
1
+ export * from './deployment.js';
2
+ export * from './drivers/index.js';
@@ -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
- if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
26
- req.originalUrl?.startsWith('/auth') === false &&
27
- env['CACHE_ENABLED'] === true &&
28
- cache &&
29
- !req.sanitizedQuery.export &&
30
- res.locals['cache'] !== false &&
31
- exceedsMaxSize === false &&
32
- (await permissionsCacheable(req.collection, {
33
- knex: getDatabase(),
34
- schema: req.schema,
35
- }, req.accountability))) {
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(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
39
- await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + getMilliseconds(env['CACHE_TTL'], 0) });
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, getMilliseconds(env['CACHE_TTL']), true, true));
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 '../../../../utils/get-relation-info.js';
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)
@@ -4,7 +4,7 @@ export interface ValidateItemAccessOptions {
4
4
  accountability: Accountability;
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
- primaryKeys: PrimaryKey[];
7
+ primaryKeys?: PrimaryKey[];
8
8
  fields?: string[];
9
9
  returnAllowedRootFields?: boolean;
10
10
  }
@@ -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 primaryKeyField = context.schema.collections[options.collection]?.primary;
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
- ast.query.filter = {
27
- [primaryKeyField]: {
28
- _in: options.primaryKeys,
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 hasAccess = items && items.length === options.primaryKeys.length;
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
+ }