@directus/api 33.3.0 → 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.
- package/dist/ai/chat/lib/create-ui-stream.js +2 -1
- package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
- package/dist/ai/chat/lib/transform-file-parts.js +36 -0
- package/dist/ai/files/adapters/anthropic.d.ts +3 -0
- package/dist/ai/files/adapters/anthropic.js +25 -0
- package/dist/ai/files/adapters/google.d.ts +3 -0
- package/dist/ai/files/adapters/google.js +58 -0
- package/dist/ai/files/adapters/index.d.ts +3 -0
- package/dist/ai/files/adapters/index.js +3 -0
- package/dist/ai/files/adapters/openai.d.ts +3 -0
- package/dist/ai/files/adapters/openai.js +22 -0
- package/dist/ai/files/controllers/upload.d.ts +2 -0
- package/dist/ai/files/controllers/upload.js +101 -0
- package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
- package/dist/ai/files/lib/fetch-provider.js +23 -0
- package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
- package/dist/ai/files/lib/upload-to-provider.js +26 -0
- package/dist/ai/files/router.d.ts +1 -0
- package/dist/ai/files/router.js +5 -0
- package/dist/ai/files/types.d.ts +5 -0
- package/dist/ai/files/types.js +1 -0
- package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
- package/dist/ai/providers/anthropic-file-support.js +94 -0
- package/dist/ai/providers/registry.js +3 -6
- package/dist/ai/tools/flows/index.d.ts +16 -16
- package/dist/ai/tools/schema.d.ts +8 -8
- package/dist/ai/tools/schema.js +2 -2
- package/dist/app.js +10 -1
- package/dist/controllers/deployment-webhooks.d.ts +2 -0
- package/dist/controllers/deployment-webhooks.js +95 -0
- package/dist/controllers/deployment.js +61 -165
- package/dist/controllers/files.js +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
- package/dist/database/helpers/date/dialects/oracle.js +2 -0
- package/dist/database/helpers/date/dialects/sqlite.js +2 -0
- package/dist/database/helpers/date/types.d.ts +1 -1
- package/dist/database/helpers/date/types.js +3 -1
- package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/mssql.js +21 -0
- package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
- package/dist/database/helpers/fn/dialects/mysql.js +30 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/oracle.js +21 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
- package/dist/database/helpers/fn/dialects/postgres.js +40 -0
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
- package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
- package/dist/database/helpers/fn/json/parse-function.js +66 -0
- package/dist/database/helpers/fn/types.d.ts +8 -0
- package/dist/database/helpers/fn/types.js +19 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +11 -0
- package/dist/database/helpers/schema/types.d.ts +1 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
- package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
- 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/operator.js +17 -7
- package/dist/database/run-ast/lib/parse-current-level.js +8 -1
- package/dist/database/run-ast/run-ast.js +11 -1
- package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
- package/dist/database/run-ast/utils/get-column.js +13 -2
- package/dist/deployment/deployment.d.ts +25 -2
- package/dist/deployment/drivers/netlify.d.ts +6 -2
- package/dist/deployment/drivers/netlify.js +114 -12
- package/dist/deployment/drivers/vercel.d.ts +5 -2
- package/dist/deployment/drivers/vercel.js +84 -5
- package/dist/deployment.d.ts +5 -0
- package/dist/deployment.js +34 -0
- package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
- package/dist/request/is-denied-ip.js +24 -23
- package/dist/services/authentication.js +27 -22
- package/dist/services/collections.js +1 -0
- package/dist/services/deployment-projects.d.ts +31 -2
- package/dist/services/deployment-projects.js +109 -5
- package/dist/services/deployment-runs.d.ts +19 -1
- package/dist/services/deployment-runs.js +86 -0
- package/dist/services/deployment.d.ts +44 -3
- package/dist/services/deployment.js +263 -15
- package/dist/services/files/utils/get-metadata.js +6 -6
- package/dist/services/files.d.ts +3 -1
- package/dist/services/files.js +26 -3
- package/dist/services/graphql/resolvers/query.js +23 -6
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +3 -0
- package/dist/services/users.js +6 -1
- package/dist/utils/get-field-relational-depth.d.ts +13 -0
- package/dist/utils/get-field-relational-depth.js +22 -0
- package/dist/utils/parse-value.d.ts +4 -0
- package/dist/utils/parse-value.js +11 -0
- package/dist/utils/sanitize-query.js +3 -2
- package/dist/utils/split-fields.d.ts +4 -0
- package/dist/utils/split-fields.js +32 -0
- package/dist/utils/validate-query.js +2 -1
- package/package.json +28 -28
|
@@ -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.
|
|
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
|
-
}
|
|
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.
|
|
300
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
301
301
|
all: "all";
|
|
302
302
|
activity: "activity";
|
|
303
|
-
}
|
|
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.
|
|
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
|
-
}
|
|
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.
|
|
340
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
341
341
|
all: "all";
|
|
342
342
|
activity: "activity";
|
|
343
|
-
}
|
|
343
|
+
}>>;
|
|
344
344
|
}, z.core.$strip>;
|
|
345
345
|
export declare const TriggerFlowInputSchema: z.ZodObject<{
|
|
346
346
|
id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
package/dist/ai/tools/schema.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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,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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
})
|
|
406
|
-
|
|
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
|
|
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
|
|
467
|
-
|
|
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 };
|