@directus/api 33.3.1 → 34.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/chat/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/fields/index.d.ts +3 -3
- package/dist/ai/tools/fields/index.js +9 -3
- 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/auth/drivers/oauth2.js +10 -4
- package/dist/auth/drivers/openid.js +10 -4
- package/dist/auth/drivers/saml.js +20 -10
- package/dist/auth/utils/resolve-login-redirect.d.ts +11 -0
- package/dist/auth/utils/resolve-login-redirect.js +62 -0
- 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/controllers/server.js +32 -26
- package/dist/controllers/tus.js +33 -2
- package/dist/controllers/utils.js +18 -0
- 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/aggregate.js +4 -4
- 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/modules/validate-access/lib/validate-item-access.js +1 -1
- 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/graphql/resolvers/system.js +35 -27
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +1 -1
- package/dist/services/users.js +6 -1
- package/dist/test-utils/README.md +112 -0
- package/dist/test-utils/controllers.d.ts +65 -0
- package/dist/test-utils/controllers.js +100 -0
- package/dist/test-utils/database.d.ts +1 -1
- package/dist/test-utils/database.js +3 -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 +36 -36
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -7
- package/dist/auth/utils/is-login-redirect-allowed.js +0 -39
|
@@ -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 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
1
2
|
import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
|
|
2
3
|
import { format } from 'date-fns';
|
|
3
4
|
import { Router } from 'express';
|
|
@@ -8,32 +9,37 @@ import { SpecificationService } from '../services/specifications.js';
|
|
|
8
9
|
import asyncHandler from '../utils/async-handler.js';
|
|
9
10
|
import { createAdmin } from '../utils/create-admin.js';
|
|
10
11
|
const router = Router();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
12
|
+
const env = useEnv();
|
|
13
|
+
if (env['OPENAPI_ENABLED'] !== false) {
|
|
14
|
+
router.get('/specs/oas', asyncHandler(async (req, res, next) => {
|
|
15
|
+
const service = new SpecificationService({
|
|
16
|
+
accountability: req.accountability,
|
|
17
|
+
schema: req.schema,
|
|
18
|
+
});
|
|
19
|
+
res.locals['payload'] = await service.oas.generate(req.headers.host);
|
|
20
|
+
return next();
|
|
21
|
+
}), respond);
|
|
22
|
+
}
|
|
23
|
+
if (env['GRAPHQL_INTROSPECTION'] !== false) {
|
|
24
|
+
router.get('/specs/graphql/:scope?', asyncHandler(async (req, res) => {
|
|
25
|
+
const service = new SpecificationService({
|
|
26
|
+
accountability: req.accountability,
|
|
27
|
+
schema: req.schema,
|
|
28
|
+
});
|
|
29
|
+
const serverService = new ServerService({
|
|
30
|
+
accountability: req.accountability,
|
|
31
|
+
schema: req.schema,
|
|
32
|
+
});
|
|
33
|
+
const scope = req.params['scope'] || 'items';
|
|
34
|
+
if (['items', 'system'].includes(scope) === false)
|
|
35
|
+
throw new RouteNotFoundError({ path: req.path });
|
|
36
|
+
const info = await serverService.serverInfo();
|
|
37
|
+
const result = await service.graphql.generate(scope);
|
|
38
|
+
const filename = info['project'].project_name + '_' + format(new Date(), 'yyyy-MM-dd') + '.graphql';
|
|
39
|
+
res.attachment(filename);
|
|
40
|
+
res.send(result);
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
37
43
|
router.get('/info', asyncHandler(async (req, res, next) => {
|
|
38
44
|
const service = new ServerService({
|
|
39
45
|
accountability: req.accountability,
|
package/dist/controllers/tus.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createError } from '@directus/errors';
|
|
2
|
+
import { ERRORS, Metadata } from '@tus/utils';
|
|
1
3
|
import { Router } from 'express';
|
|
2
4
|
import getDatabase from '../database/index.js';
|
|
3
5
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
@@ -18,11 +20,40 @@ const mapAction = (method) => {
|
|
|
18
20
|
const checkFileAccess = asyncHandler(async (req, _res, next) => {
|
|
19
21
|
if (req.accountability) {
|
|
20
22
|
const action = mapAction(req.method);
|
|
21
|
-
|
|
23
|
+
const validateAccessOptions = {
|
|
22
24
|
action,
|
|
23
25
|
collection: 'directus_files',
|
|
24
26
|
accountability: req.accountability,
|
|
25
|
-
}
|
|
27
|
+
};
|
|
28
|
+
if (req.method === 'POST' && req.header('upload-metadata')) {
|
|
29
|
+
let metadata;
|
|
30
|
+
try {
|
|
31
|
+
metadata = Metadata.parse(req.header('upload-metadata'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new (createError('INVALID_METADATA', ERRORS.INVALID_METADATA.body, ERRORS.INVALID_METADATA.status_code))();
|
|
35
|
+
}
|
|
36
|
+
// On replacement ensure update for that record
|
|
37
|
+
if (metadata['id']) {
|
|
38
|
+
validateAccessOptions.action = 'update';
|
|
39
|
+
validateAccessOptions.primaryKeys = [metadata['id']];
|
|
40
|
+
}
|
|
41
|
+
// Validate permissions for any payload fields
|
|
42
|
+
const fields = [];
|
|
43
|
+
for (const field of Object.keys(req.schema.collections['directus_files'].fields)) {
|
|
44
|
+
// PK is not mutable, access to record is already checked via `primaryKeys` for updates
|
|
45
|
+
if (field === 'id') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (field in metadata) {
|
|
49
|
+
fields.push(field);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (fields.length > 0) {
|
|
53
|
+
validateAccessOptions.fields = fields;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await validateAccess(validateAccessOptions, {
|
|
26
57
|
schema: req.schema,
|
|
27
58
|
knex: getDatabase(),
|
|
28
59
|
});
|
|
@@ -3,6 +3,7 @@ import argon2 from 'argon2';
|
|
|
3
3
|
import Busboy from 'busboy';
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import Joi from 'joi';
|
|
6
|
+
import { resolveLoginRedirect } from '../auth/utils/resolve-login-redirect.js';
|
|
6
7
|
import collectionExists from '../middleware/collection-exists.js';
|
|
7
8
|
import { respond } from '../middleware/respond.js';
|
|
8
9
|
import { ExportService, ImportService } from '../services/import-export.js';
|
|
@@ -125,4 +126,21 @@ router.post('/cache/clear', asyncHandler(async (req, res) => {
|
|
|
125
126
|
await service.clearCache({ system: clearSystemCache });
|
|
126
127
|
res.status(200).end();
|
|
127
128
|
}));
|
|
129
|
+
router.post('/resolve-redirect', asyncHandler(async (req, res) => {
|
|
130
|
+
if (!req.body?.redirect) {
|
|
131
|
+
throw new InvalidPayloadError({ reason: `"redirect" is required` });
|
|
132
|
+
}
|
|
133
|
+
if (req.body?.provider && typeof req.body.provider !== 'string') {
|
|
134
|
+
throw new InvalidPayloadError({ reason: `"provider" must be a string` });
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const resolved = resolveLoginRedirect(req.body.redirect, {
|
|
138
|
+
provider: req.body.provider,
|
|
139
|
+
});
|
|
140
|
+
return res.json({ data: resolved });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new InvalidPayloadError({ reason: `Invalid "redirect" provided` });
|
|
144
|
+
}
|
|
145
|
+
}));
|
|
128
146
|
export default router;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
|
|
2
|
-
import { getRelation, getRelationType } from '@directus/utils';
|
|
2
|
+
import { getRelation, getRelationType, parseFilterFunctionPath } from '@directus/utils';
|
|
3
3
|
import { isEmpty } from 'lodash-es';
|
|
4
4
|
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
5
5
|
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
@@ -35,13 +35,58 @@ export async function parseFields(options, context) {
|
|
|
35
35
|
name = options.query.alias[fieldKey];
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// Normalize function calls to move relational prefixes outside the function.
|
|
39
|
+
// e.g., json(m2m.data, color) → m2m.json(data, color)
|
|
40
|
+
// This allows the standard relational handling below to process the traversal
|
|
41
|
+
// uniformly for all functions (json, count, year, etc.).
|
|
42
|
+
name = parseFilterFunctionPath(name);
|
|
43
|
+
const isFunctionCall = name.includes('(') && name.includes(')');
|
|
44
|
+
if (isFunctionCall) {
|
|
45
|
+
const functionName = name.split('(')[0];
|
|
46
|
+
const columnName = name.match(REGEX_BETWEEN_PARENS)[1];
|
|
47
|
+
const foundField = context.schema.collections[options.parentCollection].fields[columnName];
|
|
48
|
+
// Create a FunctionFieldNode for relational count functions (count(related_items))
|
|
49
|
+
if (functionName === 'count' && foundField && foundField.type === 'alias') {
|
|
50
|
+
const foundRelation = context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === columnName);
|
|
51
|
+
if (foundRelation) {
|
|
52
|
+
children.push({
|
|
53
|
+
type: 'functionField',
|
|
54
|
+
name,
|
|
55
|
+
fieldKey,
|
|
56
|
+
query: {},
|
|
57
|
+
relatedCollection: foundRelation.collection,
|
|
58
|
+
whenCase: [],
|
|
59
|
+
cases: [],
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Create a FunctionFieldNode for direct (non-relational) json function calls
|
|
65
|
+
if (functionName === 'json') {
|
|
66
|
+
children.push({
|
|
67
|
+
type: 'functionField',
|
|
68
|
+
name,
|
|
69
|
+
fieldKey,
|
|
70
|
+
query: {},
|
|
71
|
+
relatedCollection: options.parentCollection,
|
|
72
|
+
whenCase: [],
|
|
73
|
+
cases: [],
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const isRelationalFunctionCall = isFunctionCall && name.includes('.') && name.indexOf('.') < name.indexOf('(');
|
|
79
|
+
const isRelational = (!isFunctionCall || isRelationalFunctionCall) &&
|
|
80
|
+
(name.includes('.') ||
|
|
81
|
+
// We'll always treat top level o2m fields as a related item. This is an alias field, otherwise it won't return
|
|
82
|
+
// anything
|
|
83
|
+
!!context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === name));
|
|
42
84
|
if (isRelational) {
|
|
43
|
-
//
|
|
44
|
-
|
|
85
|
+
// For normalized function calls, split on the resolved name since
|
|
86
|
+
// parseFilterFunctionPath may have moved relational segments outside
|
|
87
|
+
// the function args (e.g., json(m2m.data, color) → m2m.json(data, color)).
|
|
88
|
+
// For plain fields, split on fieldKey to preserve existing alias behavior.
|
|
89
|
+
const parts = (isFunctionCall ? name : fieldKey).split('.');
|
|
45
90
|
let rootField = parts[0];
|
|
46
91
|
let collectionScope = null;
|
|
47
92
|
// a2o related collection scoped field selector `fields=sections.section_id:headings.title`
|
|
@@ -72,25 +117,6 @@ export async function parseFields(options, context) {
|
|
|
72
117
|
}
|
|
73
118
|
}
|
|
74
119
|
else {
|
|
75
|
-
if (name.includes('(') && name.includes(')')) {
|
|
76
|
-
const columnName = name.match(REGEX_BETWEEN_PARENS)[1];
|
|
77
|
-
const foundField = context.schema.collections[options.parentCollection].fields[columnName];
|
|
78
|
-
if (foundField && foundField.type === 'alias') {
|
|
79
|
-
const foundRelation = context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === columnName);
|
|
80
|
-
if (foundRelation) {
|
|
81
|
-
children.push({
|
|
82
|
-
type: 'functionField',
|
|
83
|
-
name,
|
|
84
|
-
fieldKey,
|
|
85
|
-
query: {},
|
|
86
|
-
relatedCollection: foundRelation.collection,
|
|
87
|
-
whenCase: [],
|
|
88
|
-
cases: [],
|
|
89
|
-
});
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
120
|
if (name.includes(':')) {
|
|
95
121
|
const [key, scope] = name.split(':');
|
|
96
122
|
if (key in relationalStructure === false) {
|
|
@@ -3,5 +3,5 @@ export declare abstract class DateHelper extends DatabaseHelper {
|
|
|
3
3
|
parse(date: string | Date): string;
|
|
4
4
|
readTimestampString(date: string): string;
|
|
5
5
|
writeTimestamp(date: string): Date;
|
|
6
|
-
fieldFlagForField(
|
|
6
|
+
fieldFlagForField(fieldType: string): string;
|
|
7
7
|
}
|
|
@@ -11,4 +11,5 @@ export declare class FnHelperMSSQL extends FnHelper {
|
|
|
11
11
|
minute(table: string, column: string, options: FnHelperOptions): Knex.Raw;
|
|
12
12
|
second(table: string, column: string, options: FnHelperOptions): Knex.Raw;
|
|
13
13
|
count(table: string, column: string, options?: FnHelperOptions): Knex.Raw<any>;
|
|
14
|
+
json(table: string, column: string, options?: FnHelperOptions): Knex.Raw;
|
|
14
15
|
}
|