@directus/api 33.3.1 → 34.0.0

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 (98) hide show
  1. package/dist/ai/chat/lib/create-ui-stream.js +2 -1
  2. package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
  3. package/dist/ai/chat/lib/transform-file-parts.js +36 -0
  4. package/dist/ai/files/adapters/anthropic.d.ts +3 -0
  5. package/dist/ai/files/adapters/anthropic.js +25 -0
  6. package/dist/ai/files/adapters/google.d.ts +3 -0
  7. package/dist/ai/files/adapters/google.js +58 -0
  8. package/dist/ai/files/adapters/index.d.ts +3 -0
  9. package/dist/ai/files/adapters/index.js +3 -0
  10. package/dist/ai/files/adapters/openai.d.ts +3 -0
  11. package/dist/ai/files/adapters/openai.js +22 -0
  12. package/dist/ai/files/controllers/upload.d.ts +2 -0
  13. package/dist/ai/files/controllers/upload.js +101 -0
  14. package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
  15. package/dist/ai/files/lib/fetch-provider.js +23 -0
  16. package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
  17. package/dist/ai/files/lib/upload-to-provider.js +26 -0
  18. package/dist/ai/files/router.d.ts +1 -0
  19. package/dist/ai/files/router.js +5 -0
  20. package/dist/ai/files/types.d.ts +5 -0
  21. package/dist/ai/files/types.js +1 -0
  22. package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
  23. package/dist/ai/providers/anthropic-file-support.js +94 -0
  24. package/dist/ai/providers/registry.js +3 -6
  25. package/dist/ai/tools/flows/index.d.ts +16 -16
  26. package/dist/ai/tools/schema.d.ts +8 -8
  27. package/dist/ai/tools/schema.js +2 -2
  28. package/dist/app.js +10 -1
  29. package/dist/controllers/deployment-webhooks.d.ts +2 -0
  30. package/dist/controllers/deployment-webhooks.js +95 -0
  31. package/dist/controllers/deployment.js +61 -165
  32. package/dist/controllers/files.js +2 -1
  33. package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
  34. package/dist/database/helpers/date/dialects/oracle.js +2 -0
  35. package/dist/database/helpers/date/dialects/sqlite.js +2 -0
  36. package/dist/database/helpers/date/types.d.ts +1 -1
  37. package/dist/database/helpers/date/types.js +3 -1
  38. package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
  39. package/dist/database/helpers/fn/dialects/mssql.js +21 -0
  40. package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
  41. package/dist/database/helpers/fn/dialects/mysql.js +30 -0
  42. package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
  43. package/dist/database/helpers/fn/dialects/oracle.js +21 -0
  44. package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
  45. package/dist/database/helpers/fn/dialects/postgres.js +40 -0
  46. package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
  47. package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
  48. package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
  49. package/dist/database/helpers/fn/json/parse-function.js +66 -0
  50. package/dist/database/helpers/fn/types.d.ts +8 -0
  51. package/dist/database/helpers/fn/types.js +19 -0
  52. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
  53. package/dist/database/helpers/schema/dialects/mysql.js +11 -0
  54. package/dist/database/helpers/schema/types.d.ts +1 -0
  55. package/dist/database/helpers/schema/types.js +3 -0
  56. package/dist/database/index.js +2 -1
  57. package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
  58. package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
  59. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  60. package/dist/database/run-ast/lib/apply-query/filter/operator.js +17 -7
  61. package/dist/database/run-ast/lib/parse-current-level.js +8 -1
  62. package/dist/database/run-ast/run-ast.js +11 -1
  63. package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
  64. package/dist/database/run-ast/utils/get-column.js +13 -2
  65. package/dist/deployment/deployment.d.ts +25 -2
  66. package/dist/deployment/drivers/netlify.d.ts +6 -2
  67. package/dist/deployment/drivers/netlify.js +114 -12
  68. package/dist/deployment/drivers/vercel.d.ts +5 -2
  69. package/dist/deployment/drivers/vercel.js +84 -5
  70. package/dist/deployment.d.ts +5 -0
  71. package/dist/deployment.js +34 -0
  72. package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
  73. package/dist/request/is-denied-ip.js +24 -23
  74. package/dist/services/authentication.js +27 -22
  75. package/dist/services/collections.js +1 -0
  76. package/dist/services/deployment-projects.d.ts +31 -2
  77. package/dist/services/deployment-projects.js +109 -5
  78. package/dist/services/deployment-runs.d.ts +19 -1
  79. package/dist/services/deployment-runs.js +86 -0
  80. package/dist/services/deployment.d.ts +44 -3
  81. package/dist/services/deployment.js +263 -15
  82. package/dist/services/files/utils/get-metadata.js +6 -6
  83. package/dist/services/files.d.ts +3 -1
  84. package/dist/services/files.js +26 -3
  85. package/dist/services/graphql/resolvers/query.js +23 -6
  86. package/dist/services/payload.d.ts +6 -0
  87. package/dist/services/payload.js +27 -2
  88. package/dist/services/server.js +1 -1
  89. package/dist/services/users.js +6 -1
  90. package/dist/utils/get-field-relational-depth.d.ts +13 -0
  91. package/dist/utils/get-field-relational-depth.js +22 -0
  92. package/dist/utils/parse-value.d.ts +4 -0
  93. package/dist/utils/parse-value.js +11 -0
  94. package/dist/utils/sanitize-query.js +3 -2
  95. package/dist/utils/split-fields.d.ts +4 -0
  96. package/dist/utils/split-fields.js +32 -0
  97. package/dist/utils/validate-query.js +2 -1
  98. package/package.json +29 -29
@@ -272,13 +272,13 @@ export declare const FlowItemInputSchema: z.ZodObject<{
272
272
  active: "active";
273
273
  inactive: "inactive";
274
274
  }>>;
275
- trigger: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
275
+ trigger: z.ZodOptional<z.ZodEnum<{
276
276
  operation: "operation";
277
277
  schedule: "schedule";
278
278
  event: "event";
279
279
  webhook: "webhook";
280
280
  manual: "manual";
281
- }>, z.ZodNull]>>;
281
+ }>>;
282
282
  options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
283
283
  operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
284
284
  operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -297,10 +297,10 @@ export declare const FlowItemInputSchema: z.ZodObject<{
297
297
  }, z.core.$strip>>>;
298
298
  date_created: z.ZodOptional<z.ZodString>;
299
299
  user_created: z.ZodOptional<z.ZodString>;
300
- accountability: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
300
+ accountability: z.ZodOptional<z.ZodEnum<{
301
301
  all: "all";
302
302
  activity: "activity";
303
- }>, z.ZodNull]>>;
303
+ }>>;
304
304
  }, z.core.$strip>;
305
305
  export declare const FlowItemValidateSchema: z.ZodObject<{
306
306
  id: z.ZodOptional<z.ZodString>;
@@ -312,13 +312,13 @@ export declare const FlowItemValidateSchema: z.ZodObject<{
312
312
  active: "active";
313
313
  inactive: "inactive";
314
314
  }>>;
315
- trigger: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
315
+ trigger: z.ZodOptional<z.ZodEnum<{
316
316
  operation: "operation";
317
317
  schedule: "schedule";
318
318
  event: "event";
319
319
  webhook: "webhook";
320
320
  manual: "manual";
321
- }>, z.ZodNull]>>;
321
+ }>>;
322
322
  options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
323
323
  operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
324
324
  operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -337,10 +337,10 @@ export declare const FlowItemValidateSchema: z.ZodObject<{
337
337
  }, z.core.$strip>>>;
338
338
  date_created: z.ZodOptional<z.ZodString>;
339
339
  user_created: z.ZodOptional<z.ZodString>;
340
- accountability: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
340
+ accountability: z.ZodOptional<z.ZodEnum<{
341
341
  all: "all";
342
342
  activity: "activity";
343
- }>, z.ZodNull]>>;
343
+ }>>;
344
344
  }, z.core.$strip>;
345
345
  export declare const TriggerFlowInputSchema: z.ZodObject<{
346
346
  id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
@@ -135,13 +135,13 @@ export const FlowItemInputSchema = z
135
135
  color: z.union([z.string(), z.null()]),
136
136
  description: z.union([z.string(), z.null()]),
137
137
  status: z.enum(['active', 'inactive']),
138
- trigger: z.union([z.enum(['event', 'schedule', 'operation', 'webhook', 'manual']), z.null()]),
138
+ trigger: z.enum(['event', 'schedule', 'operation', 'webhook', 'manual']),
139
139
  options: z.union([z.record(z.string(), z.any()), z.null()]),
140
140
  operation: z.union([z.string(), z.null()]),
141
141
  operations: z.array(OperationItemInputSchema),
142
142
  date_created: z.string(),
143
143
  user_created: z.string(),
144
- accountability: z.union([z.enum(['all', 'activity']), z.null()]),
144
+ accountability: z.enum(['all', 'activity']),
145
145
  })
146
146
  .partial();
147
147
  export const FlowItemValidateSchema = FlowItemInputSchema;
package/dist/app.js CHANGED
@@ -10,6 +10,7 @@ import express from 'express';
10
10
  import { merge } from 'lodash-es';
11
11
  import qs from 'qs';
12
12
  import { aiChatRouter } from './ai/chat/router.js';
13
+ import { aiFilesRouter } from './ai/files/router.js';
13
14
  import { registerAuthProviders } from './auth.js';
14
15
  import accessRouter from './controllers/access.js';
15
16
  import activityRouter from './controllers/activity.js';
@@ -18,6 +19,7 @@ import authRouter from './controllers/auth.js';
18
19
  import collectionsRouter from './controllers/collections.js';
19
20
  import commentsRouter from './controllers/comments.js';
20
21
  import dashboardsRouter from './controllers/dashboards.js';
22
+ import deploymentWebhookRouter from './controllers/deployment-webhooks.js';
21
23
  import deploymentRouter from './controllers/deployment.js';
22
24
  import extensionsRouter from './controllers/extensions.js';
23
25
  import fieldsRouter from './controllers/fields.js';
@@ -48,7 +50,7 @@ import usersRouter from './controllers/users.js';
48
50
  import utilsRouter from './controllers/utils.js';
49
51
  import versionsRouter from './controllers/versions.js';
50
52
  import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations, } from './database/index.js';
51
- import { registerDeploymentDrivers } from './deployment.js';
53
+ import { ensureDeploymentWebhooks, registerDeploymentDrivers } from './deployment.js';
52
54
  import emitter from './emitter.js';
53
55
  import { getExtensionManager } from './extensions/index.js';
54
56
  import { getFlowManager } from './flows.js';
@@ -96,6 +98,7 @@ export default async function createApp() {
96
98
  await validateStorage();
97
99
  await registerAuthProviders();
98
100
  registerDeploymentDrivers();
101
+ await ensureDeploymentWebhooks();
99
102
  const extensionManager = getExtensionManager();
100
103
  const flowManager = getFlowManager();
101
104
  await extensionManager.initialize();
@@ -161,6 +164,9 @@ export default async function createApp() {
161
164
  app.use((req, res, next) => {
162
165
  express.json({
163
166
  limit: env['MAX_PAYLOAD_SIZE'],
167
+ verify: (req, _res, buf) => {
168
+ req.rawBody = buf;
169
+ },
164
170
  })(req, res, (err) => {
165
171
  if (err) {
166
172
  return next(new InvalidPayloadError({ reason: err.message }));
@@ -214,6 +220,8 @@ export default async function createApp() {
214
220
  app.use(rateLimiter);
215
221
  }
216
222
  app.get('/server/ping', (_req, res) => res.send('pong'));
223
+ // Public webhook endpoint (signature-verified by the provider)
224
+ app.use('/deployments/webhooks', deploymentWebhookRouter);
217
225
  app.use(authenticate);
218
226
  app.use(schema);
219
227
  app.use(sanitizeQuery);
@@ -243,6 +251,7 @@ export default async function createApp() {
243
251
  }
244
252
  if (toBoolean(env['AI_ENABLED']) === true) {
245
253
  app.use('/ai/chat', aiChatRouter);
254
+ app.use('/ai/files', aiFilesRouter);
246
255
  }
247
256
  if (env['METRICS_ENABLED'] === true) {
248
257
  app.use('/metrics', metricsRouter);
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,95 @@
1
+ import { DEPLOYMENT_PROVIDER_TYPES } from '@directus/types';
2
+ import express from 'express';
3
+ import getDatabase from '../database/index.js';
4
+ import { getDeploymentDriver } from '../deployment.js';
5
+ import emitter from '../emitter.js';
6
+ import { useLogger } from '../logger/index.js';
7
+ import { DeploymentProjectsService } from '../services/deployment-projects.js';
8
+ import { DeploymentRunsService } from '../services/deployment-runs.js';
9
+ import { DeploymentService } from '../services/deployment.js';
10
+ import asyncHandler from '../utils/async-handler.js';
11
+ import { getSchema } from '../utils/get-schema.js';
12
+ const router = express.Router();
13
+ router.post('/:provider', asyncHandler(async (req, res) => {
14
+ const logger = useLogger();
15
+ const provider = req.params['provider'];
16
+ if (!DEPLOYMENT_PROVIDER_TYPES.includes(provider)) {
17
+ return res.sendStatus(404);
18
+ }
19
+ const rawBody = req.rawBody;
20
+ if (!rawBody) {
21
+ logger.debug(`[webhook:${provider}] No raw body`);
22
+ return res.sendStatus(400);
23
+ }
24
+ const schema = await getSchema();
25
+ const knex = getDatabase();
26
+ const deploymentService = new DeploymentService({
27
+ schema,
28
+ knex,
29
+ accountability: null,
30
+ });
31
+ let webhookConfig;
32
+ try {
33
+ webhookConfig = await deploymentService.getWebhookConfig(provider);
34
+ }
35
+ catch {
36
+ logger.warn(`[webhook:${provider}] No webhook config found`);
37
+ return res.sendStatus(404);
38
+ }
39
+ if (!webhookConfig.webhook_secret) {
40
+ logger.warn(`[webhook:${provider}] No webhook secret configured`);
41
+ return res.sendStatus(404);
42
+ }
43
+ const driver = getDeploymentDriver(provider, webhookConfig.credentials, webhookConfig.options);
44
+ // Fallback for providers whose API doesn't support webhook signature headers (e.g. Netlify)
45
+ const headers = { ...req.headers };
46
+ const queryToken = req.query['token'];
47
+ if (typeof queryToken === 'string') {
48
+ headers['x-webhook-token'] = queryToken;
49
+ }
50
+ const event = driver.verifyAndParseWebhook(rawBody, headers, webhookConfig.webhook_secret);
51
+ if (!event) {
52
+ logger.warn(`[webhook:${provider}] Verification failed or unknown event`);
53
+ try {
54
+ const body = JSON.parse(rawBody.toString('utf-8'));
55
+ logger.warn(`[webhook:${provider}] Raw event type: ${body.type ?? body.state ?? 'unknown'}`);
56
+ }
57
+ catch {
58
+ logger.warn(`[webhook:${provider}] Unparseable body: ${rawBody.toString('utf-8').slice(0, 200)}`);
59
+ }
60
+ return res.sendStatus(401);
61
+ }
62
+ // Look up project by external_id
63
+ const projectsService = new DeploymentProjectsService({
64
+ schema,
65
+ knex,
66
+ accountability: null,
67
+ });
68
+ const project = await projectsService.readByExternalId(event.project_external_id);
69
+ if (!project) {
70
+ // 410 signals the provider this project is no longer tracked
71
+ logger.info(`[webhook:${provider}] Project ${event.project_external_id} not tracked`);
72
+ return res.sendStatus(410);
73
+ }
74
+ const runsService = new DeploymentRunsService({
75
+ schema,
76
+ knex,
77
+ accountability: null,
78
+ });
79
+ const runId = await runsService.processWebhookEvent(project.id, event);
80
+ // Emit action events
81
+ const eventPayload = {
82
+ provider,
83
+ project_id: project.id,
84
+ run_id: runId,
85
+ external_id: event.deployment_external_id,
86
+ status: event.status,
87
+ url: event.url,
88
+ target: event.target,
89
+ timestamp: event.timestamp,
90
+ };
91
+ emitter.emitAction(['deployment.webhook', `deployment.webhook.${event.type}`], eventPayload, null);
92
+ logger.info(`[webhook:${provider}] Processed: ${event.type} → run ${runId}`);
93
+ return res.sendStatus(200);
94
+ }));
95
+ export default router;
@@ -1,8 +1,9 @@
1
- import { ErrorCode, ForbiddenError, InvalidPathParameterError, InvalidPayloadError, isDirectusError, } from '@directus/errors';
1
+ import { ErrorCode, InvalidPathParameterError, InvalidPayloadError, isDirectusError } from '@directus/errors';
2
2
  import { DEPLOYMENT_PROVIDER_TYPES } from '@directus/types';
3
3
  import express from 'express';
4
4
  import Joi from 'joi';
5
5
  import getDatabase from '../database/index.js';
6
+ import { useLogger } from '../logger/index.js';
6
7
  import { respond } from '../middleware/respond.js';
7
8
  import useCollection from '../middleware/use-collection.js';
8
9
  import { validateBatch } from '../middleware/validate-batch.js';
@@ -11,16 +12,14 @@ import { DeploymentRunsService } from '../services/deployment-runs.js';
11
12
  import { DeploymentService } from '../services/deployment.js';
12
13
  import { MetaService } from '../services/meta.js';
13
14
  import asyncHandler from '../utils/async-handler.js';
15
+ import { getMilliseconds } from '../utils/get-milliseconds.js';
14
16
  import { transaction } from '../utils/transaction.js';
15
17
  const router = express.Router();
18
+ function parseRange(range, defaultMs) {
19
+ const ms = getMilliseconds(range, defaultMs);
20
+ return new Date(Date.now() - ms);
21
+ }
16
22
  router.use(useCollection('directus_deployments'));
17
- // Require admin access for all deployment routes
18
- router.use((_req, _res, next) => {
19
- if (_req.accountability && _req.accountability.admin !== true) {
20
- throw new ForbiddenError();
21
- }
22
- return next();
23
- });
24
23
  // Validate provider parameter
25
24
  const validateProvider = (provider) => {
26
25
  return DEPLOYMENT_PROVIDER_TYPES.includes(provider);
@@ -101,40 +100,9 @@ router.get('/:provider/projects', asyncHandler(async (req, res, next) => {
101
100
  accountability: req.accountability,
102
101
  schema: req.schema,
103
102
  });
104
- // Get provider config to find deployment ID
105
103
  const deployment = await service.readByProvider(provider);
106
- // Get projects from provider (with cache)
107
104
  const { data: providerProjects, remainingTTL } = await service.listProviderProjects(provider);
108
- // Get selected projects from DB
109
- const selectedProjects = await projectsService.readByQuery({
110
- filter: { deployment: { _eq: deployment.id } },
111
- });
112
- // Map by external_id for quick lookup
113
- const selectedMap = new Map(selectedProjects.map((p) => [p.external_id, p]));
114
- // Sync names from provider
115
- const namesToUpdate = selectedProjects
116
- .map((dbProject) => {
117
- const providerProject = providerProjects.find((p) => p.id === dbProject.external_id);
118
- if (providerProject && providerProject.name !== dbProject.name) {
119
- return { id: dbProject.id, name: providerProject.name };
120
- }
121
- return null;
122
- })
123
- .filter((update) => update !== null);
124
- if (namesToUpdate.length > 0) {
125
- await projectsService.updateBatch(namesToUpdate);
126
- }
127
- // Merge with DB structure (id !== null means selected)
128
- const projects = providerProjects.map((project) => {
129
- return {
130
- id: selectedMap.get(project.id)?.id ?? null,
131
- external_id: project.id,
132
- name: project.name,
133
- deployable: project.deployable,
134
- framework: project.framework,
135
- };
136
- });
137
- // Pass remaining TTL for response headers
105
+ const projects = await projectsService.listWithSync(deployment.id, providerProjects);
138
106
  res.locals['cache'] = false;
139
107
  res.locals['cacheTTL'] = remainingTTL;
140
108
  res.locals['payload'] = { data: projects };
@@ -198,64 +166,42 @@ router.patch('/:provider/projects', asyncHandler(async (req, res, next) => {
198
166
  accountability: req.accountability,
199
167
  schema: req.schema,
200
168
  });
201
- // Get provider config
202
- const deployment = await service.readByProvider(provider);
203
169
  // Validate deployable projects before any mutation
204
170
  if (value.create.length > 0) {
205
- const driver = await service.getDriver(provider);
206
- const providerProjects = await driver.listProjects();
207
- const projectsMap = new Map(providerProjects.map((p) => [p.id, p]));
208
- const nonDeployable = value.create.filter((p) => !projectsMap.get(p.external_id)?.deployable);
209
- if (nonDeployable.length > 0) {
210
- const names = nonDeployable
211
- .map((p) => projectsMap.get(p.external_id)?.name || p.external_id)
212
- .join(', ');
213
- throw new InvalidPayloadError({
214
- reason: `Cannot add non-deployable projects: ${names}`,
215
- });
216
- }
171
+ await projectsService.validateDeployable(provider, value.create);
217
172
  }
218
- const updatedProjects = await projectsService.updateSelection(deployment.id, value.create, value.delete);
173
+ const updatedProjects = await projectsService.updateSelection(provider, value.create, value.delete);
174
+ // Sync webhook with updated project list
175
+ service.syncWebhook(provider).catch((err) => {
176
+ const logger = useLogger();
177
+ logger.error(`Failed to sync webhook for ${provider}: ${err}`);
178
+ });
219
179
  res.locals['payload'] = { data: updatedProjects };
220
180
  return next();
221
181
  }), respond);
182
+ const rangeQuerySchema = Joi.object({
183
+ range: Joi.string()
184
+ .pattern(/^\d+(ms|s|m|h|d|w|y)$/)
185
+ .optional(),
186
+ });
222
187
  // Dashboard - selected projects with stats
223
188
  router.get('/:provider/dashboard', asyncHandler(async (req, res, next) => {
224
189
  const provider = req.params['provider'];
225
190
  if (!validateProvider(provider)) {
226
191
  throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
227
192
  }
193
+ const { error, value } = rangeQuerySchema.validate(req.query);
194
+ if (error) {
195
+ throw new InvalidPayloadError({ reason: error.message });
196
+ }
197
+ const sinceDate = parseRange(value.range, 86_400_000);
228
198
  const service = new DeploymentService({
229
199
  accountability: req.accountability,
230
200
  schema: req.schema,
231
201
  });
232
- const projectsService = new DeploymentProjectsService({
233
- accountability: req.accountability,
234
- schema: req.schema,
235
- });
236
- // Get provider config
237
- const deployment = await service.readByProvider(provider);
238
- // Get selected projects from DB
239
- const selectedProjects = await projectsService.readByQuery({
240
- filter: { deployment: { _eq: deployment.id } },
241
- });
242
- if (selectedProjects.length === 0) {
243
- res.locals['payload'] = { data: { projects: [] } };
244
- return next();
245
- }
246
- // Fetch full details for each selected project (parallel)
247
- const driver = await service.getDriver(provider);
248
- const projectDetails = await Promise.all(selectedProjects.map(async (p) => {
249
- const details = await driver.getProject(p.external_id);
250
- return {
251
- ...details,
252
- id: p.id,
253
- external_id: p.external_id,
254
- };
255
- }));
256
- // Disable cache - dashboard needs fresh data from provider
202
+ const data = await service.getDashboard(provider, sinceDate);
257
203
  res.locals['cache'] = false;
258
- res.locals['payload'] = { data: { projects: projectDetails } };
204
+ res.locals['payload'] = { data };
259
205
  return next();
260
206
  }), respond);
261
207
  // Trigger deployment for a project
@@ -277,36 +223,11 @@ router.post('/:provider/projects/:id/deploy', asyncHandler(async (req, res, next
277
223
  accountability: req.accountability,
278
224
  schema: req.schema,
279
225
  });
280
- const projectsService = new DeploymentProjectsService({
281
- accountability: req.accountability,
282
- schema: req.schema,
283
- });
284
- const runsService = new DeploymentRunsService({
285
- accountability: req.accountability,
286
- schema: req.schema,
287
- });
288
- // Get project from DB
289
- const project = await projectsService.readOne(projectId);
290
- // Trigger deployment via driver
291
- const driver = await service.getDriver(provider);
292
- const result = await driver.triggerDeployment(project.external_id, {
226
+ const run = await service.triggerDeployment(provider, projectId, {
293
227
  preview: value.preview,
294
228
  clearCache: value.clear_cache,
295
229
  });
296
- // Store run in DB
297
- const runId = await runsService.createOne({
298
- project: projectId,
299
- external_id: result.deployment_id,
300
- target: value.preview ? 'preview' : 'production',
301
- });
302
- const run = await runsService.readOne(runId);
303
- res.locals['payload'] = {
304
- data: {
305
- ...run,
306
- status: result.status,
307
- url: result.url,
308
- },
309
- };
230
+ res.locals['payload'] = { data: run };
310
231
  return next();
311
232
  }), respond);
312
233
  // Update deployment config by provider
@@ -356,17 +277,11 @@ router.delete('/:provider', asyncHandler(async (req, _res, next) => {
356
277
  }), respond);
357
278
  // List runs for a project
358
279
  router.get('/:provider/projects/:id/runs', asyncHandler(async (req, res, next) => {
359
- // Disable cache - runs status needs to be fresh from provider
360
- res.locals['cache'] = false;
361
280
  const provider = req.params['provider'];
362
281
  const projectId = req.params['id'];
363
282
  if (!validateProvider(provider)) {
364
283
  throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
365
284
  }
366
- const service = new DeploymentService({
367
- accountability: req.accountability,
368
- schema: req.schema,
369
- });
370
285
  const projectsService = new DeploymentProjectsService({
371
286
  accountability: req.accountability,
372
287
  schema: req.schema,
@@ -377,7 +292,6 @@ router.get('/:provider/projects/:id/runs', asyncHandler(async (req, res, next) =
377
292
  });
378
293
  // Validate project exists
379
294
  await projectsService.readOne(projectId);
380
- // Get paginated runs from DB (default limit: 10)
381
295
  const query = {
382
296
  ...req.sanitizedQuery,
383
297
  filter: { project: { _eq: projectId } },
@@ -386,24 +300,39 @@ router.get('/:provider/projects/:id/runs', asyncHandler(async (req, res, next) =
386
300
  fields: ['*', 'user_created.first_name', 'user_created.last_name', 'user_created.email'],
387
301
  };
388
302
  const runs = await runsService.readByQuery(query);
389
- // Get pagination meta
390
303
  const metaService = new MetaService({
391
304
  accountability: req.accountability,
392
305
  schema: req.schema,
393
306
  });
394
307
  const meta = await metaService.getMetaForQuery('directus_deployment_runs', query);
395
- // Fetch status for each run from provider
396
- const driver = await service.getDriver(provider);
397
- const runsWithStatus = await Promise.all(runs.map(async (run) => {
398
- const details = await driver.getDeployment(run.external_id);
399
- return {
400
- ...run,
401
- ...details,
402
- id: run.id,
403
- external_id: run.external_id,
404
- };
405
- }));
406
- res.locals['payload'] = { data: runsWithStatus, meta };
308
+ res.locals['payload'] = { data: runs, meta };
309
+ return next();
310
+ }), respond);
311
+ // Project runs stats
312
+ router.get('/:provider/projects/:id/runs/stats', asyncHandler(async (req, res, next) => {
313
+ const provider = req.params['provider'];
314
+ const projectId = req.params['id'];
315
+ if (!validateProvider(provider)) {
316
+ throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
317
+ }
318
+ const { error, value } = rangeQuerySchema.validate(req.query);
319
+ if (error) {
320
+ throw new InvalidPayloadError({ reason: error.message });
321
+ }
322
+ const sinceDate = parseRange(value.range, 604_800_000).toISOString();
323
+ const projectsService = new DeploymentProjectsService({
324
+ accountability: req.accountability,
325
+ schema: req.schema,
326
+ });
327
+ const runsService = new DeploymentRunsService({
328
+ accountability: req.accountability,
329
+ schema: req.schema,
330
+ });
331
+ // Validate project exists and user has access
332
+ await projectsService.readOne(projectId);
333
+ const data = await runsService.getStats(projectId, sinceDate);
334
+ res.locals['cache'] = false;
335
+ res.locals['payload'] = { data };
407
336
  return next();
408
337
  }), respond);
409
338
  // Get single run details
@@ -421,31 +350,13 @@ router.get('/:provider/runs/:id', asyncHandler(async (req, res, next) => {
421
350
  if (error) {
422
351
  throw new InvalidPayloadError({ reason: error.message });
423
352
  }
424
- const sinceDate = value.since;
425
- const runsService = new DeploymentRunsService({
426
- accountability: req.accountability,
427
- schema: req.schema,
428
- });
429
353
  const service = new DeploymentService({
430
354
  accountability: req.accountability,
431
355
  schema: req.schema,
432
356
  });
433
- const run = await runsService.readOne(runId);
434
- const driver = await service.getDriver(provider);
435
- const [details, logs] = await Promise.all([
436
- driver.getDeployment(run.external_id),
437
- driver.getDeploymentLogs(run.external_id, sinceDate ? { since: sinceDate } : undefined),
438
- ]);
357
+ const data = await service.getRunWithLogs(provider, runId, value.since);
439
358
  res.locals['cache'] = false;
440
- res.locals['payload'] = {
441
- data: {
442
- ...run,
443
- ...details,
444
- id: run.id,
445
- external_id: run.external_id,
446
- logs,
447
- },
448
- };
359
+ res.locals['payload'] = { data };
449
360
  return next();
450
361
  }), respond);
451
362
  // Cancel a deployment
@@ -455,27 +366,12 @@ router.post('/:provider/runs/:id/cancel', asyncHandler(async (req, res, next) =>
455
366
  if (!validateProvider(provider)) {
456
367
  throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
457
368
  }
458
- const runsService = new DeploymentRunsService({
459
- accountability: req.accountability,
460
- schema: req.schema,
461
- });
462
369
  const service = new DeploymentService({
463
370
  accountability: req.accountability,
464
371
  schema: req.schema,
465
372
  });
466
- const run = await runsService.readOne(runId);
467
- const driver = await service.getDriver(provider);
468
- await driver.cancelDeployment(run.external_id);
469
- // Fetch updated status
470
- const details = await driver.getDeployment(run.external_id);
471
- res.locals['payload'] = {
472
- data: {
473
- ...run,
474
- ...details,
475
- id: run.id,
476
- external_id: run.external_id,
477
- },
478
- };
373
+ const data = await service.cancelDeployment(provider, runId);
374
+ res.locals['payload'] = { data };
479
375
  return next();
480
376
  }), respond);
481
377
  export default router;
@@ -150,6 +150,7 @@ router.post('/', asyncHandler(multipartHandler), asyncHandler(async (req, res, n
150
150
  const importSchema = Joi.object({
151
151
  url: Joi.string().required(),
152
152
  data: Joi.object(),
153
+ options: Joi.object({ filterMimeType: Joi.array().items(Joi.string()) }),
153
154
  });
154
155
  router.post('/import', asyncHandler(async (req, res, next) => {
155
156
  const { error } = importSchema.validate(req.body);
@@ -160,7 +161,7 @@ router.post('/import', asyncHandler(async (req, res, next) => {
160
161
  accountability: req.accountability,
161
162
  schema: req.schema,
162
163
  });
163
- const primaryKey = await service.importOne(req.body.url, req.body.data);
164
+ const primaryKey = await service.importOne(req.body.url, req.body.data, req.body.options);
164
165
  try {
165
166
  const record = await service.readOne(primaryKey, req.sanitizedQuery);
166
167
  res.locals['payload'] = { data: record || null };