@directus/api 33.0.0 → 33.1.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/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +122 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/items/index.js +4 -1
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/schema.js +1 -1
- package/dist/app.js +4 -0
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +142 -137
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +25 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/controllers/deployment.d.ts +2 -0
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/fields.js +6 -4
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/middleware/respond.js +27 -14
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
- package/dist/server.js +2 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/server.js +1 -0
- package/dist/services/specifications.js +2 -2
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-service.js +7 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/package.json +32 -30
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { ErrorCode, ForbiddenError, InvalidPathParameterError, InvalidPayloadError, isDirectusError, } from '@directus/errors';
|
|
2
|
+
import { DEPLOYMENT_PROVIDER_TYPES } from '@directus/types';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import Joi from 'joi';
|
|
5
|
+
import getDatabase from '../database/index.js';
|
|
6
|
+
import { respond } from '../middleware/respond.js';
|
|
7
|
+
import useCollection from '../middleware/use-collection.js';
|
|
8
|
+
import { validateBatch } from '../middleware/validate-batch.js';
|
|
9
|
+
import { DeploymentProjectsService } from '../services/deployment-projects.js';
|
|
10
|
+
import { DeploymentRunsService } from '../services/deployment-runs.js';
|
|
11
|
+
import { DeploymentService } from '../services/deployment.js';
|
|
12
|
+
import { MetaService } from '../services/meta.js';
|
|
13
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
14
|
+
import { transaction } from '../utils/transaction.js';
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
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
|
+
// Validate provider parameter
|
|
25
|
+
const validateProvider = (provider) => {
|
|
26
|
+
return DEPLOYMENT_PROVIDER_TYPES.includes(provider);
|
|
27
|
+
};
|
|
28
|
+
// Validation schema for creating/updating deployment
|
|
29
|
+
const deploymentSchema = Joi.object({
|
|
30
|
+
provider: Joi.string()
|
|
31
|
+
.valid(...DEPLOYMENT_PROVIDER_TYPES)
|
|
32
|
+
.required(),
|
|
33
|
+
credentials: Joi.object().required(),
|
|
34
|
+
options: Joi.object(),
|
|
35
|
+
}).unknown();
|
|
36
|
+
// Create deployment config
|
|
37
|
+
router.post('/', asyncHandler(async (req, res, next) => {
|
|
38
|
+
const { error } = deploymentSchema.validate(req.body);
|
|
39
|
+
if (error) {
|
|
40
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
41
|
+
}
|
|
42
|
+
const db = getDatabase();
|
|
43
|
+
const item = await transaction(db, async (trx) => {
|
|
44
|
+
const service = new DeploymentService({
|
|
45
|
+
accountability: req.accountability,
|
|
46
|
+
schema: req.schema,
|
|
47
|
+
knex: trx,
|
|
48
|
+
});
|
|
49
|
+
const key = await service.createOne({
|
|
50
|
+
provider: req.body.provider,
|
|
51
|
+
credentials: req.body.credentials,
|
|
52
|
+
options: req.body.options,
|
|
53
|
+
});
|
|
54
|
+
return service.readOne(key, req.sanitizedQuery);
|
|
55
|
+
});
|
|
56
|
+
res.locals['payload'] = { data: item };
|
|
57
|
+
return next();
|
|
58
|
+
}), respond);
|
|
59
|
+
// Read all deployment configs
|
|
60
|
+
const readHandler = asyncHandler(async (req, res, next) => {
|
|
61
|
+
const service = new DeploymentService({
|
|
62
|
+
accountability: req.accountability,
|
|
63
|
+
schema: req.schema,
|
|
64
|
+
});
|
|
65
|
+
const metaService = new MetaService({
|
|
66
|
+
accountability: req.accountability,
|
|
67
|
+
schema: req.schema,
|
|
68
|
+
});
|
|
69
|
+
const records = await service.readByQuery(req.sanitizedQuery);
|
|
70
|
+
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
|
71
|
+
res.locals['payload'] = { data: records || null, meta };
|
|
72
|
+
return next();
|
|
73
|
+
});
|
|
74
|
+
router.get('/', validateBatch('read'), readHandler, respond);
|
|
75
|
+
router.search('/', validateBatch('read'), readHandler, respond);
|
|
76
|
+
// Read single deployment config by provider
|
|
77
|
+
router.get('/:provider', asyncHandler(async (req, res, next) => {
|
|
78
|
+
const provider = req.params['provider'];
|
|
79
|
+
if (!validateProvider(provider)) {
|
|
80
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
81
|
+
}
|
|
82
|
+
const service = new DeploymentService({
|
|
83
|
+
accountability: req.accountability,
|
|
84
|
+
schema: req.schema,
|
|
85
|
+
});
|
|
86
|
+
const record = await service.readByProvider(provider, req.sanitizedQuery);
|
|
87
|
+
res.locals['payload'] = { data: record || null };
|
|
88
|
+
return next();
|
|
89
|
+
}), respond);
|
|
90
|
+
// List projects from provider (for config/selection)
|
|
91
|
+
router.get('/:provider/projects', asyncHandler(async (req, res, next) => {
|
|
92
|
+
const provider = req.params['provider'];
|
|
93
|
+
if (!validateProvider(provider)) {
|
|
94
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
95
|
+
}
|
|
96
|
+
const service = new DeploymentService({
|
|
97
|
+
accountability: req.accountability,
|
|
98
|
+
schema: req.schema,
|
|
99
|
+
});
|
|
100
|
+
const projectsService = new DeploymentProjectsService({
|
|
101
|
+
accountability: req.accountability,
|
|
102
|
+
schema: req.schema,
|
|
103
|
+
});
|
|
104
|
+
// Get provider config to find deployment ID
|
|
105
|
+
const deployment = await service.readByProvider(provider);
|
|
106
|
+
// Get projects from provider (with cache)
|
|
107
|
+
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
|
|
138
|
+
res.locals['cache'] = false;
|
|
139
|
+
res.locals['cacheTTL'] = remainingTTL;
|
|
140
|
+
res.locals['payload'] = { data: projects };
|
|
141
|
+
return next();
|
|
142
|
+
}), respond);
|
|
143
|
+
// Get single project details
|
|
144
|
+
router.get('/:provider/projects/:id', asyncHandler(async (req, res, next) => {
|
|
145
|
+
const provider = req.params['provider'];
|
|
146
|
+
const projectId = req.params['id'];
|
|
147
|
+
if (!validateProvider(provider)) {
|
|
148
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
149
|
+
}
|
|
150
|
+
const service = new DeploymentService({
|
|
151
|
+
accountability: req.accountability,
|
|
152
|
+
schema: req.schema,
|
|
153
|
+
});
|
|
154
|
+
const projectsService = new DeploymentProjectsService({
|
|
155
|
+
accountability: req.accountability,
|
|
156
|
+
schema: req.schema,
|
|
157
|
+
});
|
|
158
|
+
// Get project from DB (validates it exists and is selected)
|
|
159
|
+
const project = await projectsService.readOne(projectId);
|
|
160
|
+
// Fetch details from provider using external_id (with cache)
|
|
161
|
+
const { data: details, remainingTTL } = await service.getProviderProject(provider, project.external_id);
|
|
162
|
+
// Pass remaining TTL for response headers
|
|
163
|
+
res.locals['cache'] = false;
|
|
164
|
+
res.locals['cacheTTL'] = remainingTTL;
|
|
165
|
+
res.locals['payload'] = {
|
|
166
|
+
data: {
|
|
167
|
+
...details,
|
|
168
|
+
id: project.id,
|
|
169
|
+
external_id: project.external_id,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
return next();
|
|
173
|
+
}), respond);
|
|
174
|
+
// Update selected projects
|
|
175
|
+
const updateProjectsSchema = Joi.object({
|
|
176
|
+
create: Joi.array()
|
|
177
|
+
.items(Joi.object({
|
|
178
|
+
external_id: Joi.string().required(),
|
|
179
|
+
name: Joi.string().required(),
|
|
180
|
+
}))
|
|
181
|
+
.default([]),
|
|
182
|
+
delete: Joi.array().items(Joi.string()).default([]),
|
|
183
|
+
});
|
|
184
|
+
router.patch('/:provider/projects', asyncHandler(async (req, res, next) => {
|
|
185
|
+
const provider = req.params['provider'];
|
|
186
|
+
if (!validateProvider(provider)) {
|
|
187
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
188
|
+
}
|
|
189
|
+
const { error, value } = updateProjectsSchema.validate(req.body);
|
|
190
|
+
if (error) {
|
|
191
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
192
|
+
}
|
|
193
|
+
const service = new DeploymentService({
|
|
194
|
+
accountability: req.accountability,
|
|
195
|
+
schema: req.schema,
|
|
196
|
+
});
|
|
197
|
+
const projectsService = new DeploymentProjectsService({
|
|
198
|
+
accountability: req.accountability,
|
|
199
|
+
schema: req.schema,
|
|
200
|
+
});
|
|
201
|
+
// Get provider config
|
|
202
|
+
const deployment = await service.readByProvider(provider);
|
|
203
|
+
// Validate deployable projects before any mutation
|
|
204
|
+
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
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const updatedProjects = await projectsService.updateSelection(deployment.id, value.create, value.delete);
|
|
219
|
+
res.locals['payload'] = { data: updatedProjects };
|
|
220
|
+
return next();
|
|
221
|
+
}), respond);
|
|
222
|
+
// Dashboard - selected projects with stats
|
|
223
|
+
router.get('/:provider/dashboard', asyncHandler(async (req, res, next) => {
|
|
224
|
+
const provider = req.params['provider'];
|
|
225
|
+
if (!validateProvider(provider)) {
|
|
226
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
227
|
+
}
|
|
228
|
+
const service = new DeploymentService({
|
|
229
|
+
accountability: req.accountability,
|
|
230
|
+
schema: req.schema,
|
|
231
|
+
});
|
|
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
|
|
257
|
+
res.locals['cache'] = false;
|
|
258
|
+
res.locals['payload'] = { data: { projects: projectDetails } };
|
|
259
|
+
return next();
|
|
260
|
+
}), respond);
|
|
261
|
+
// Trigger deployment for a project
|
|
262
|
+
const triggerDeploySchema = Joi.object({
|
|
263
|
+
preview: Joi.boolean().default(false),
|
|
264
|
+
clear_cache: Joi.boolean().default(true), // Default at true (matches Vercel UI behavior)
|
|
265
|
+
});
|
|
266
|
+
router.post('/:provider/projects/:id/deploy', asyncHandler(async (req, res, next) => {
|
|
267
|
+
const provider = req.params['provider'];
|
|
268
|
+
const projectId = req.params['id'];
|
|
269
|
+
if (!validateProvider(provider)) {
|
|
270
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
271
|
+
}
|
|
272
|
+
const { error, value } = triggerDeploySchema.validate(req.body);
|
|
273
|
+
if (error) {
|
|
274
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
275
|
+
}
|
|
276
|
+
const service = new DeploymentService({
|
|
277
|
+
accountability: req.accountability,
|
|
278
|
+
schema: req.schema,
|
|
279
|
+
});
|
|
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, {
|
|
293
|
+
preview: value.preview,
|
|
294
|
+
clearCache: value.clear_cache,
|
|
295
|
+
});
|
|
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
|
+
};
|
|
310
|
+
return next();
|
|
311
|
+
}), respond);
|
|
312
|
+
// Update deployment config by provider
|
|
313
|
+
router.patch('/:provider', asyncHandler(async (req, res, next) => {
|
|
314
|
+
const provider = req.params['provider'];
|
|
315
|
+
if (!validateProvider(provider)) {
|
|
316
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
317
|
+
}
|
|
318
|
+
const db = getDatabase();
|
|
319
|
+
const item = await transaction(db, async (trx) => {
|
|
320
|
+
const service = new DeploymentService({
|
|
321
|
+
accountability: req.accountability,
|
|
322
|
+
schema: req.schema,
|
|
323
|
+
knex: trx,
|
|
324
|
+
});
|
|
325
|
+
const data = {};
|
|
326
|
+
if ('credentials' in req.body)
|
|
327
|
+
data['credentials'] = req.body.credentials;
|
|
328
|
+
if ('options' in req.body)
|
|
329
|
+
data['options'] = req.body.options;
|
|
330
|
+
const primaryKey = await service.updateByProvider(provider, data);
|
|
331
|
+
try {
|
|
332
|
+
return await service.readOne(primaryKey, req.sanitizedQuery);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
res.locals['payload'] = { data: item };
|
|
342
|
+
return next();
|
|
343
|
+
}), respond);
|
|
344
|
+
// Delete deployment config by provider
|
|
345
|
+
router.delete('/:provider', asyncHandler(async (req, _res, next) => {
|
|
346
|
+
const provider = req.params['provider'];
|
|
347
|
+
if (!validateProvider(provider)) {
|
|
348
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
349
|
+
}
|
|
350
|
+
const service = new DeploymentService({
|
|
351
|
+
accountability: req.accountability,
|
|
352
|
+
schema: req.schema,
|
|
353
|
+
});
|
|
354
|
+
await service.deleteByProvider(provider);
|
|
355
|
+
return next();
|
|
356
|
+
}), respond);
|
|
357
|
+
// List runs for a project
|
|
358
|
+
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
|
+
const provider = req.params['provider'];
|
|
362
|
+
const projectId = req.params['id'];
|
|
363
|
+
if (!validateProvider(provider)) {
|
|
364
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
365
|
+
}
|
|
366
|
+
const service = new DeploymentService({
|
|
367
|
+
accountability: req.accountability,
|
|
368
|
+
schema: req.schema,
|
|
369
|
+
});
|
|
370
|
+
const projectsService = new DeploymentProjectsService({
|
|
371
|
+
accountability: req.accountability,
|
|
372
|
+
schema: req.schema,
|
|
373
|
+
});
|
|
374
|
+
const runsService = new DeploymentRunsService({
|
|
375
|
+
accountability: req.accountability,
|
|
376
|
+
schema: req.schema,
|
|
377
|
+
});
|
|
378
|
+
// Validate project exists
|
|
379
|
+
await projectsService.readOne(projectId);
|
|
380
|
+
// Get paginated runs from DB (default limit: 10)
|
|
381
|
+
const query = {
|
|
382
|
+
...req.sanitizedQuery,
|
|
383
|
+
filter: { project: { _eq: projectId } },
|
|
384
|
+
sort: ['-date_created'],
|
|
385
|
+
limit: req.sanitizedQuery.limit ?? 10,
|
|
386
|
+
fields: ['*', 'user_created.first_name', 'user_created.last_name', 'user_created.email'],
|
|
387
|
+
};
|
|
388
|
+
const runs = await runsService.readByQuery(query);
|
|
389
|
+
// Get pagination meta
|
|
390
|
+
const metaService = new MetaService({
|
|
391
|
+
accountability: req.accountability,
|
|
392
|
+
schema: req.schema,
|
|
393
|
+
});
|
|
394
|
+
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 };
|
|
407
|
+
return next();
|
|
408
|
+
}), respond);
|
|
409
|
+
// Get single run details
|
|
410
|
+
const runDetailsQuerySchema = Joi.object({
|
|
411
|
+
since: Joi.date().iso().optional(),
|
|
412
|
+
_t: Joi.number().optional(), // Cache-buster parameter for polling
|
|
413
|
+
});
|
|
414
|
+
router.get('/:provider/runs/:id', asyncHandler(async (req, res, next) => {
|
|
415
|
+
const provider = req.params['provider'];
|
|
416
|
+
const runId = req.params['id'];
|
|
417
|
+
if (!validateProvider(provider)) {
|
|
418
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
419
|
+
}
|
|
420
|
+
const { error, value } = runDetailsQuerySchema.validate(req.query);
|
|
421
|
+
if (error) {
|
|
422
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
423
|
+
}
|
|
424
|
+
const sinceDate = value.since;
|
|
425
|
+
const runsService = new DeploymentRunsService({
|
|
426
|
+
accountability: req.accountability,
|
|
427
|
+
schema: req.schema,
|
|
428
|
+
});
|
|
429
|
+
const service = new DeploymentService({
|
|
430
|
+
accountability: req.accountability,
|
|
431
|
+
schema: req.schema,
|
|
432
|
+
});
|
|
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
|
+
]);
|
|
439
|
+
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
|
+
};
|
|
449
|
+
return next();
|
|
450
|
+
}), respond);
|
|
451
|
+
// Cancel a deployment
|
|
452
|
+
router.post('/:provider/runs/:id/cancel', asyncHandler(async (req, res, next) => {
|
|
453
|
+
const provider = req.params['provider'];
|
|
454
|
+
const runId = req.params['id'];
|
|
455
|
+
if (!validateProvider(provider)) {
|
|
456
|
+
throw new InvalidPathParameterError({ reason: `${provider} is not a supported provider` });
|
|
457
|
+
}
|
|
458
|
+
const runsService = new DeploymentRunsService({
|
|
459
|
+
accountability: req.accountability,
|
|
460
|
+
schema: req.schema,
|
|
461
|
+
});
|
|
462
|
+
const service = new DeploymentService({
|
|
463
|
+
accountability: req.accountability,
|
|
464
|
+
schema: req.schema,
|
|
465
|
+
});
|
|
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
|
+
};
|
|
479
|
+
return next();
|
|
480
|
+
}), respond);
|
|
481
|
+
export default router;
|
|
@@ -91,8 +91,9 @@ router.patch('/:collection', validateCollection, asyncHandler(async (req, res, n
|
|
|
91
91
|
for (const fieldData of req.body) {
|
|
92
92
|
if (isSystemField(req.params['collection'], fieldData['field'])) {
|
|
93
93
|
const { error } = systemFieldUpdateSchema.safeParse(fieldData);
|
|
94
|
-
if (error)
|
|
95
|
-
throw
|
|
94
|
+
if (error) {
|
|
95
|
+
throw new InvalidPayloadError({ reason: 'Only "schema.is_indexed" may be modified for system fields' });
|
|
96
|
+
}
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
await service.updateFields(req.params['collection'], req.body, {
|
|
@@ -134,8 +135,9 @@ router.patch('/:collection/:field', validateCollection, asyncHandler(async (req,
|
|
|
134
135
|
});
|
|
135
136
|
if (isSystemField(req.params['collection'], req.params['field'])) {
|
|
136
137
|
const { error } = systemFieldUpdateSchema.safeParse(req.body);
|
|
137
|
-
if (error)
|
|
138
|
-
throw
|
|
138
|
+
if (error) {
|
|
139
|
+
throw new InvalidPayloadError({ reason: 'Only "schema.is_indexed" may be modified for system fields' });
|
|
140
|
+
}
|
|
139
141
|
}
|
|
140
142
|
else {
|
|
141
143
|
const { error } = updateSchema.validate(req.body);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
|
|
2
|
-
import { getRelation } from '@directus/utils';
|
|
2
|
+
import { getRelation, getRelationType } 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';
|
|
6
|
-
import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
7
6
|
import { getAllowedSort } from '../utils/get-allowed-sort.js';
|
|
8
7
|
import { getDeepQuery } from '../utils/get-deep-query.js';
|
|
9
8
|
import { getRelatedCollection } from '../utils/get-related-collection.js';
|
|
@@ -118,6 +117,7 @@ export async function parseFields(options, context) {
|
|
|
118
117
|
relation,
|
|
119
118
|
collection: options.parentCollection,
|
|
120
119
|
field: fieldName,
|
|
120
|
+
useA2O: true,
|
|
121
121
|
});
|
|
122
122
|
if (!relationType)
|
|
123
123
|
continue;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const DEFAULT_OPENAI_MODELS = ['gpt-5-nano', 'gpt-5-mini', 'gpt-5'];
|
|
2
|
+
const DEFAULT_ANTHROPIC_MODELS = ['claude-haiku-4-5', 'claude-sonnet-4-5'];
|
|
3
|
+
const DEFAULT_GOOGLE_MODELS = ['gemini-3-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-pro', 'gemini-2.5-flash'];
|
|
4
|
+
export async function up(knex) {
|
|
5
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
6
|
+
table.text('ai_google_api_key');
|
|
7
|
+
table.text('ai_openai_compatible_api_key');
|
|
8
|
+
table.text('ai_openai_compatible_base_url');
|
|
9
|
+
table.text('ai_openai_compatible_name');
|
|
10
|
+
table.json('ai_openai_compatible_models');
|
|
11
|
+
table.json('ai_openai_compatible_headers');
|
|
12
|
+
table.json('ai_openai_allowed_models');
|
|
13
|
+
table.json('ai_anthropic_allowed_models');
|
|
14
|
+
table.json('ai_google_allowed_models');
|
|
15
|
+
});
|
|
16
|
+
// Set default allowed models for existing installations
|
|
17
|
+
await knex('directus_settings').update({
|
|
18
|
+
ai_openai_allowed_models: JSON.stringify(DEFAULT_OPENAI_MODELS),
|
|
19
|
+
ai_anthropic_allowed_models: JSON.stringify(DEFAULT_ANTHROPIC_MODELS),
|
|
20
|
+
ai_google_allowed_models: JSON.stringify(DEFAULT_GOOGLE_MODELS),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function down(knex) {
|
|
24
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
25
|
+
table.dropColumn('ai_google_api_key');
|
|
26
|
+
table.dropColumn('ai_openai_compatible_api_key');
|
|
27
|
+
table.dropColumn('ai_openai_compatible_base_url');
|
|
28
|
+
table.dropColumn('ai_openai_compatible_name');
|
|
29
|
+
table.dropColumn('ai_openai_compatible_models');
|
|
30
|
+
table.dropColumn('ai_openai_compatible_headers');
|
|
31
|
+
table.dropColumn('ai_openai_allowed_models');
|
|
32
|
+
table.dropColumn('ai_anthropic_allowed_models');
|
|
33
|
+
table.dropColumn('ai_google_allowed_models');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
3
|
+
table.boolean('collaborative_editing_enabled').defaultTo(false).notNullable();
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
8
|
+
table.dropColumn('collaborative_editing_enabled');
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.createTable('directus_deployments', (table) => {
|
|
3
|
+
table.uuid('id').primary().notNullable();
|
|
4
|
+
table.string('provider').notNullable().unique();
|
|
5
|
+
table.text('credentials');
|
|
6
|
+
table.text('options');
|
|
7
|
+
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
8
|
+
table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
|
|
9
|
+
});
|
|
10
|
+
await knex.schema.createTable('directus_deployment_projects', (table) => {
|
|
11
|
+
table.uuid('id').primary().notNullable();
|
|
12
|
+
table.uuid('deployment').notNullable().references('id').inTable('directus_deployments').onDelete('CASCADE');
|
|
13
|
+
table.string('external_id').notNullable();
|
|
14
|
+
table.string('name').notNullable();
|
|
15
|
+
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
16
|
+
table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
|
|
17
|
+
table.unique(['deployment', 'external_id']);
|
|
18
|
+
});
|
|
19
|
+
await knex.schema.createTable('directus_deployment_runs', (table) => {
|
|
20
|
+
table.uuid('id').primary().notNullable();
|
|
21
|
+
table.uuid('project').notNullable().references('id').inTable('directus_deployment_projects').onDelete('CASCADE');
|
|
22
|
+
table.string('external_id').notNullable();
|
|
23
|
+
table.string('target').notNullable(); // 'production' or 'preview'
|
|
24
|
+
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
25
|
+
table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function down(knex) {
|
|
29
|
+
await knex.schema.dropTable('directus_deployment_runs');
|
|
30
|
+
await knex.schema.dropTable('directus_deployment_projects');
|
|
31
|
+
await knex.schema.dropTable('directus_deployments');
|
|
32
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
+
import { getRelationInfo } from '@directus/utils';
|
|
2
3
|
import { clone } from 'lodash-es';
|
|
3
|
-
import { getRelationInfo } from '../../../../utils/get-relation-info.js';
|
|
4
4
|
import { getHelpers } from '../../../helpers/index.js';
|
|
5
5
|
import { generateJoinAlias } from '../../utils/generate-alias.js';
|
|
6
6
|
export function addJoin({ path, collection, aliasMap, rootQuery, schema, knex }) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FieldOverview } from '@directus/types';
|
|
2
2
|
export declare function getFilterType(fields: Record<string, FieldOverview>, key: string, collection?: string): {
|
|
3
|
-
type: "string" | "boolean" | "binary" | "
|
|
3
|
+
type: "string" | "boolean" | "binary" | "integer" | "unknown" | "date" | "text" | "json" | "float" | "alias" | "uuid" | "time" | "dateTime" | "timestamp" | "bigInteger" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon";
|
|
4
4
|
special?: never;
|
|
5
5
|
} | {
|
|
6
|
-
type: "string" | "boolean" | "binary" | "
|
|
6
|
+
type: "string" | "boolean" | "binary" | "integer" | "unknown" | "date" | "text" | "json" | "float" | "alias" | "uuid" | "time" | "dateTime" | "timestamp" | "bigInteger" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon";
|
|
7
7
|
special: string[];
|
|
8
8
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
+
import { getRelationInfo } from '@directus/utils';
|
|
2
3
|
import { getCases } from '../../../../../permissions/modules/process-ast/lib/get-cases.js';
|
|
3
4
|
import { getColumnPath } from '../../../../../utils/get-column-path.js';
|
|
4
|
-
import { getRelationInfo } from '../../../../../utils/get-relation-info.js';
|
|
5
5
|
import { getHelpers } from '../../../../helpers/index.js';
|
|
6
6
|
import { addJoin } from '../add-join.js';
|
|
7
7
|
import { getFilterPath } from '../get-filter-path.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { getRelationInfo } from '@directus/utils';
|
|
1
2
|
import { getColumnPath } from '../../../../utils/get-column-path.js';
|
|
2
|
-
import { getRelationInfo } from '../../../../utils/get-relation-info.js';
|
|
3
3
|
import { getColumn } from '../../utils/get-column.js';
|
|
4
4
|
import { addJoin } from './add-join.js';
|
|
5
5
|
export function applySort(knex, schema, rootQuery, sort, aggregate, collection, aliasMap, returnRecords = false) {
|