@directus/api 32.2.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.
Files changed (297) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +8 -7
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +122 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/assets/index.js +1 -1
  22. package/dist/ai/tools/collections/index.js +2 -2
  23. package/dist/ai/tools/fields/index.js +2 -2
  24. package/dist/ai/tools/files/index.js +1 -1
  25. package/dist/ai/tools/flows/index.js +1 -1
  26. package/dist/ai/tools/folders/index.js +1 -1
  27. package/dist/ai/tools/items/index.js +6 -3
  28. package/dist/ai/tools/items/prompt.md +7 -9
  29. package/dist/ai/tools/relations/index.js +1 -1
  30. package/dist/ai/tools/schema.js +1 -1
  31. package/dist/ai/tools/trigger-flow/index.js +1 -1
  32. package/dist/app.js +12 -8
  33. package/dist/auth/drivers/ldap.d.ts +1 -1
  34. package/dist/auth/drivers/ldap.js +144 -139
  35. package/dist/auth/drivers/local.js +1 -1
  36. package/dist/auth/drivers/oauth2.d.ts +1 -2
  37. package/dist/auth/drivers/oauth2.js +22 -17
  38. package/dist/auth/drivers/openid.d.ts +1 -2
  39. package/dist/auth/drivers/openid.js +18 -13
  40. package/dist/auth/drivers/saml.js +3 -3
  41. package/dist/auth/utils/generate-callback-url.d.ts +11 -0
  42. package/dist/auth/utils/generate-callback-url.js +40 -0
  43. package/dist/auth/utils/is-login-redirect-allowed.d.ts +7 -0
  44. package/dist/{utils → auth/utils}/is-login-redirect-allowed.js +12 -9
  45. package/dist/cache.d.ts +12 -0
  46. package/dist/cache.js +27 -3
  47. package/dist/cli/commands/bootstrap/index.js +2 -2
  48. package/dist/cli/commands/database/install.js +1 -1
  49. package/dist/cli/commands/database/migrate.js +1 -1
  50. package/dist/cli/commands/init/index.js +2 -2
  51. package/dist/cli/commands/roles/create.js +4 -4
  52. package/dist/cli/commands/schema/apply.js +3 -3
  53. package/dist/cli/commands/schema/snapshot.js +1 -1
  54. package/dist/cli/utils/create-db-connection.d.ts +1 -1
  55. package/dist/cli/utils/create-db-connection.js +1 -1
  56. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  57. package/dist/cli/utils/create-env/index.js +1 -1
  58. package/dist/constants.d.ts +7 -3
  59. package/dist/constants.js +7 -3
  60. package/dist/controllers/access.js +1 -1
  61. package/dist/controllers/assets.js +1 -1
  62. package/dist/controllers/deployment.js +481 -0
  63. package/dist/controllers/extensions.js +1 -1
  64. package/dist/controllers/fields.js +8 -6
  65. package/dist/controllers/files.js +1 -1
  66. package/dist/controllers/items.js +1 -1
  67. package/dist/controllers/not-found.js +1 -1
  68. package/dist/controllers/relations.js +1 -1
  69. package/dist/database/errors/dialects/mysql.d.ts +1 -1
  70. package/dist/database/errors/dialects/postgres.d.ts +1 -1
  71. package/dist/database/errors/dialects/sqlite.d.ts +1 -1
  72. package/dist/database/errors/translate.d.ts +1 -1
  73. package/dist/database/errors/translate.js +1 -1
  74. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  75. package/dist/database/helpers/date/dialects/mssql.js +1 -1
  76. package/dist/database/helpers/date/dialects/mysql.js +1 -1
  77. package/dist/database/helpers/date/types.js +1 -1
  78. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
  79. package/dist/database/helpers/schema/dialects/cockroachdb.js +24 -1
  80. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  81. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  82. package/dist/database/helpers/schema/dialects/mysql.js +16 -3
  83. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  84. package/dist/database/helpers/schema/types.d.ts +13 -0
  85. package/dist/database/helpers/schema/types.js +24 -0
  86. package/dist/database/index.js +4 -4
  87. package/dist/database/migrations/20220429A-add-flows.js +1 -1
  88. package/dist/database/migrations/20230526A-migrate-translation-strings.js +1 -1
  89. package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +1 -1
  90. package/dist/database/migrations/20240204A-marketplace.js +9 -7
  91. package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +15 -0
  92. package/dist/database/migrations/20240311A-deprecate-webhooks.js +1 -1
  93. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  94. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +1 -1
  95. package/dist/database/migrations/20251014A-add-project-owner.js +1 -1
  96. package/dist/database/migrations/20251224A-remove-webhooks.d.ts +3 -0
  97. package/dist/database/migrations/20251224A-remove-webhooks.js +19 -0
  98. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  99. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  100. package/dist/database/migrations/20260113A-add-revisions-index.d.ts +3 -0
  101. package/dist/database/migrations/20260113A-add-revisions-index.js +41 -0
  102. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  103. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  104. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  105. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  106. package/dist/database/migrations/run.js +3 -3
  107. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  108. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  109. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +1 -1
  110. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  111. package/dist/database/run-ast/lib/apply-query/filter/operator.js +1 -1
  112. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  113. package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
  114. package/dist/database/run-ast/utils/get-column.js +1 -1
  115. package/dist/database/seeds/run.js +3 -3
  116. package/dist/deployment/deployment.d.ts +94 -0
  117. package/dist/deployment/deployment.js +29 -0
  118. package/dist/deployment/drivers/index.d.ts +1 -0
  119. package/dist/deployment/drivers/index.js +1 -0
  120. package/dist/deployment/drivers/vercel.d.ts +32 -0
  121. package/dist/deployment/drivers/vercel.js +208 -0
  122. package/dist/deployment/index.d.ts +2 -0
  123. package/dist/deployment/index.js +2 -0
  124. package/dist/deployment.d.ts +24 -0
  125. package/dist/deployment.js +39 -0
  126. package/dist/extensions/lib/get-extensions-path.js +1 -1
  127. package/dist/extensions/lib/get-extensions-settings.js +1 -1
  128. package/dist/extensions/lib/get-extensions.js +1 -1
  129. package/dist/extensions/lib/get-shared-deps-mapping.js +3 -3
  130. package/dist/extensions/lib/installation/manager.js +3 -3
  131. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -1
  132. package/dist/extensions/lib/sync/status.js +1 -1
  133. package/dist/extensions/lib/sync/sync.js +7 -7
  134. package/dist/extensions/lib/sync/utils.js +2 -2
  135. package/dist/extensions/manager.d.ts +1 -1
  136. package/dist/extensions/manager.js +8 -8
  137. package/dist/flows.d.ts +1 -1
  138. package/dist/logger/index.js +1 -1
  139. package/dist/logger/logs-stream.d.ts +1 -1
  140. package/dist/logger/logs-stream.js +1 -1
  141. package/dist/mailer.js +1 -1
  142. package/dist/metrics/lib/create-metrics.js +2 -2
  143. package/dist/middleware/authenticate.js +3 -3
  144. package/dist/middleware/collection-exists.js +1 -1
  145. package/dist/middleware/extract-token.js +1 -1
  146. package/dist/middleware/graphql.js +2 -2
  147. package/dist/middleware/respond.js +27 -14
  148. package/dist/middleware/validate-batch.js +1 -1
  149. package/dist/operations/exec/index.js +2 -1
  150. package/dist/operations/mail/index.js +1 -1
  151. package/dist/operations/mail/rate-limiter.js +2 -2
  152. package/dist/permissions/cache.js +5 -0
  153. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -1
  154. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -1
  155. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -2
  156. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -1
  157. package/dist/permissions/modules/process-ast/process-ast.js +1 -1
  158. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  159. package/dist/permissions/modules/process-payload/process-payload.js +1 -1
  160. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +14 -2
  161. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +72 -13
  162. package/dist/permissions/modules/validate-access/validate-access.js +3 -2
  163. package/dist/rate-limiter.js +1 -1
  164. package/dist/request/is-denied-ip.js +1 -1
  165. package/dist/schedules/project.js +1 -1
  166. package/dist/schedules/telemetry.js +1 -1
  167. package/dist/schedules/tus.js +1 -1
  168. package/dist/server.js +6 -5
  169. package/dist/services/assets.d.ts +2 -1
  170. package/dist/services/assets.js +35 -8
  171. package/dist/services/authentication.js +2 -2
  172. package/dist/services/collections.js +1 -1
  173. package/dist/services/deployment-projects.d.ts +20 -0
  174. package/dist/services/deployment-projects.js +34 -0
  175. package/dist/services/deployment-runs.d.ts +13 -0
  176. package/dist/services/deployment-runs.js +6 -0
  177. package/dist/services/deployment.d.ts +40 -0
  178. package/dist/services/deployment.js +202 -0
  179. package/dist/services/extensions.d.ts +1 -1
  180. package/dist/services/files/utils/get-metadata.d.ts +1 -1
  181. package/dist/services/files/utils/get-metadata.js +1 -1
  182. package/dist/services/files.d.ts +1 -1
  183. package/dist/services/files.js +4 -4
  184. package/dist/services/graphql/index.d.ts +1 -1
  185. package/dist/services/graphql/index.js +1 -1
  186. package/dist/services/graphql/resolvers/mutation.js +1 -1
  187. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  188. package/dist/services/graphql/schema/get-types.d.ts +1 -1
  189. package/dist/services/graphql/schema/read.js +1 -1
  190. package/dist/services/graphql/subscription.d.ts +1 -1
  191. package/dist/services/graphql/types/date.js +1 -1
  192. package/dist/services/graphql/types/hash.js +1 -1
  193. package/dist/services/graphql/utils/add-path-to-validation-error.js +1 -1
  194. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  195. package/dist/services/import-export.d.ts +1 -1
  196. package/dist/services/import-export.js +2 -2
  197. package/dist/services/index.d.ts +3 -1
  198. package/dist/services/index.js +3 -1
  199. package/dist/services/mail/index.js +2 -2
  200. package/dist/services/mail/rate-limiter.js +2 -2
  201. package/dist/services/payload.js +2 -2
  202. package/dist/services/schema.js +1 -1
  203. package/dist/services/server.js +13 -4
  204. package/dist/services/settings.js +2 -2
  205. package/dist/services/specifications.js +2 -2
  206. package/dist/services/tfa.js +1 -1
  207. package/dist/services/translations.js +1 -1
  208. package/dist/services/tus/data-store.d.ts +1 -3
  209. package/dist/services/tus/data-store.js +2 -5
  210. package/dist/services/tus/server.js +6 -6
  211. package/dist/services/users.js +4 -4
  212. package/dist/services/versions.js +1 -1
  213. package/dist/telemetry/lib/get-report.js +2 -0
  214. package/dist/telemetry/lib/send-report.d.ts +1 -1
  215. package/dist/telemetry/lib/send-report.js +1 -1
  216. package/dist/telemetry/lib/track.js +1 -1
  217. package/dist/telemetry/types/report.d.ts +8 -0
  218. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  219. package/dist/telemetry/utils/get-settings.js +5 -0
  220. package/dist/test-utils/knex.js +1 -1
  221. package/dist/types/collection.d.ts +1 -1
  222. package/dist/utils/async-handler.d.ts +1 -1
  223. package/dist/utils/calculate-field-depth.js +1 -1
  224. package/dist/utils/compress.js +1 -1
  225. package/dist/utils/deep-map-response.d.ts +1 -1
  226. package/dist/utils/deep-map-response.js +2 -2
  227. package/dist/utils/get-cache-key.js +1 -1
  228. package/dist/utils/get-column-path.js +1 -1
  229. package/dist/utils/get-field-system-rows.js +1 -1
  230. package/dist/utils/get-ip-from-req.d.ts +1 -1
  231. package/dist/utils/get-ip-from-req.js +1 -1
  232. package/dist/utils/get-local-type.js +7 -3
  233. package/dist/utils/get-service.js +7 -3
  234. package/dist/utils/get-snapshot-diff.js +1 -1
  235. package/dist/utils/is-field-allowed.d.ts +4 -0
  236. package/dist/utils/is-field-allowed.js +9 -0
  237. package/dist/utils/is-url-allowed.js +1 -1
  238. package/dist/utils/jwt.js +1 -1
  239. package/dist/utils/sanitize-schema.d.ts +1 -1
  240. package/dist/utils/should-clear-cache.d.ts +1 -1
  241. package/dist/utils/should-skip-cache.js +2 -2
  242. package/dist/utils/validate-diff.js +1 -1
  243. package/dist/utils/validate-snapshot.js +3 -3
  244. package/dist/utils/validate-storage.js +2 -2
  245. package/dist/utils/verify-session-jwt.js +1 -1
  246. package/dist/utils/versioning/handle-version.js +1 -1
  247. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  248. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  249. package/dist/websocket/collab/collab.d.ts +63 -0
  250. package/dist/websocket/collab/collab.js +481 -0
  251. package/dist/websocket/collab/constants.d.ts +1 -0
  252. package/dist/websocket/collab/constants.js +13 -0
  253. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  254. package/dist/websocket/collab/filter-to-fields.js +11 -0
  255. package/dist/websocket/collab/messenger.d.ts +43 -0
  256. package/dist/websocket/collab/messenger.js +225 -0
  257. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  258. package/dist/websocket/collab/payload-permissions.js +158 -0
  259. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  260. package/dist/websocket/collab/permissions-cache.js +204 -0
  261. package/dist/websocket/collab/room.d.ts +125 -0
  262. package/dist/websocket/collab/room.js +593 -0
  263. package/dist/websocket/collab/store.d.ts +7 -0
  264. package/dist/websocket/collab/store.js +33 -0
  265. package/dist/websocket/collab/types.d.ts +21 -0
  266. package/dist/websocket/collab/types.js +1 -0
  267. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  268. package/dist/websocket/collab/verify-permissions.js +100 -0
  269. package/dist/websocket/controllers/base.d.ts +2 -2
  270. package/dist/websocket/controllers/base.js +3 -3
  271. package/dist/websocket/controllers/graphql.d.ts +1 -1
  272. package/dist/websocket/controllers/graphql.js +1 -1
  273. package/dist/websocket/controllers/logs.d.ts +1 -1
  274. package/dist/websocket/controllers/rest.d.ts +1 -1
  275. package/dist/websocket/controllers/rest.js +2 -2
  276. package/dist/websocket/handlers/heartbeat.js +1 -1
  277. package/dist/websocket/handlers/index.d.ts +2 -0
  278. package/dist/websocket/handlers/index.js +9 -0
  279. package/dist/websocket/handlers/items.js +2 -2
  280. package/dist/websocket/handlers/subscribe.js +1 -1
  281. package/dist/websocket/types.d.ts +1 -1
  282. package/dist/websocket/utils/items.d.ts +2 -2
  283. package/dist/websocket/utils/message.d.ts +1 -1
  284. package/dist/websocket/utils/message.js +2 -2
  285. package/dist/websocket/utils/wait-for-message.js +1 -1
  286. package/package.json +35 -33
  287. package/dist/controllers/webhooks.js +0 -74
  288. package/dist/services/webhooks.d.ts +0 -14
  289. package/dist/services/webhooks.js +0 -32
  290. package/dist/utils/get-relation-info.d.ts +0 -6
  291. package/dist/utils/get-relation-info.js +0 -43
  292. package/dist/utils/get-relation-type.d.ts +0 -6
  293. package/dist/utils/get-relation-type.js +0 -18
  294. package/dist/utils/is-login-redirect-allowed.d.ts +0 -4
  295. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  296. package/dist/utils/versioning/deep-map-with-schema.js +0 -81
  297. /package/dist/controllers/{webhooks.d.ts → deployment.d.ts} +0 -0
@@ -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;
@@ -1,6 +1,6 @@
1
+ import { EXTENSION_TYPES } from '@directus/constants';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
3
- import { EXTENSION_TYPES } from '@directus/constants';
4
4
  import { account, describe, list, } from '@directus/extensions-registry';
5
5
  import { isIn } from '@directus/utils';
6
6
  import express from 'express';
@@ -1,15 +1,15 @@
1
1
  import { TYPES } from '@directus/constants';
2
+ import { ErrorCode, InvalidPayloadError } from '@directus/errors';
2
3
  import { ForbiddenError, isDirectusError } from '@directus/errors';
4
+ import { isSystemField } from '@directus/system-data';
3
5
  import { Router } from 'express';
4
6
  import Joi from 'joi';
5
7
  import { ALIAS_TYPES } from '../constants.js';
6
- import { ErrorCode, InvalidPayloadError } from '@directus/errors';
7
8
  import validateCollection from '../middleware/collection-exists.js';
8
9
  import { respond } from '../middleware/respond.js';
9
10
  import useCollection from '../middleware/use-collection.js';
10
11
  import { FieldsService, systemFieldUpdateSchema } from '../services/fields.js';
11
12
  import asyncHandler from '../utils/async-handler.js';
12
- import { isSystemField } from '@directus/system-data';
13
13
  const router = Router();
14
14
  router.use(useCollection('directus_fields'));
15
15
  router.get('/', asyncHandler(async (req, res, next) => {
@@ -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 error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
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 error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
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,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
4
  import formatTitle from '@directus/format-title';
@@ -7,7 +8,6 @@ import bytes from 'bytes';
7
8
  import express from 'express';
8
9
  import Joi from 'joi';
9
10
  import { minimatch } from 'minimatch';
10
- import path from 'path';
11
11
  import { respond } from '../middleware/respond.js';
12
12
  import useCollection from '../middleware/use-collection.js';
13
13
  import { validateBatch } from '../middleware/validate-batch.js';
@@ -1,4 +1,4 @@
1
- import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
2
2
  import { isSystemCollection } from '@directus/system-data';
3
3
  import express from 'express';
4
4
  import collectionExists from '../middleware/collection-exists.js';
@@ -1,6 +1,6 @@
1
+ import { RouteNotFoundError } from '@directus/errors';
1
2
  import getDatabase from '../database/index.js';
2
3
  import emitter from '../emitter.js';
3
- import { RouteNotFoundError } from '@directus/errors';
4
4
  /**
5
5
  * Handles not found routes.
6
6
  *
@@ -1,7 +1,7 @@
1
+ import { ErrorCode, InvalidPayloadError } from '@directus/errors';
1
2
  import { isDirectusError } from '@directus/errors';
2
3
  import express from 'express';
3
4
  import Joi from 'joi';
4
- import { ErrorCode, InvalidPayloadError } from '@directus/errors';
5
5
  import validateCollection from '../middleware/collection-exists.js';
6
6
  import { respond } from '../middleware/respond.js';
7
7
  import useCollection from '../middleware/use-collection.js';
@@ -1,3 +1,3 @@
1
- import type { MySQLError } from './types.js';
2
1
  import type { Item } from '@directus/types';
2
+ import type { MySQLError } from './types.js';
3
3
  export declare function extractError(error: MySQLError, data: Partial<Item>): MySQLError | Error;
@@ -1,3 +1,3 @@
1
- import type { PostgresError } from './types.js';
2
1
  import type { Item } from '@directus/types';
2
+ import type { PostgresError } from './types.js';
3
3
  export declare function extractError(error: PostgresError, data: Partial<Item>): PostgresError | Error;
@@ -1,3 +1,3 @@
1
- import type { SQLiteError } from './types.js';
2
1
  import type { Item } from '@directus/types';
2
+ import type { SQLiteError } from './types.js';
3
3
  export declare function extractError(error: SQLiteError, data: Partial<Item>): SQLiteError | Error;
@@ -1,5 +1,5 @@
1
- import type { SQLError } from './dialects/types.js';
2
1
  import type { Item } from '@directus/types';
2
+ import type { SQLError } from './dialects/types.js';
3
3
  /**
4
4
  * Translates an error thrown by any of the databases into a pre-defined Exception. Currently
5
5
  * supports:
@@ -1,5 +1,5 @@
1
- import getDatabase, { getDatabaseClient } from '../index.js';
2
1
  import emitter from '../../emitter.js';
2
+ import getDatabase, { getDatabaseClient } from '../index.js';
3
3
  import { extractError as mssql } from './dialects/mssql.js';
4
4
  import { extractError as mysql } from './dialects/mysql.js';
5
5
  import { extractError as oracle } from './dialects/oracle.js';
@@ -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;
@@ -1,5 +1,5 @@
1
- import { DateHelper } from '../types.js';
2
1
  import { parseISO } from 'date-fns';
2
+ import { DateHelper } from '../types.js';
3
3
  export class DateHelperMSSQL extends DateHelper {
4
4
  writeTimestamp(date) {
5
5
  const parsedDate = parseISO(date);
@@ -1,5 +1,5 @@
1
- import { DateHelper } from '../types.js';
2
1
  import { parseISO } from 'date-fns';
2
+ import { DateHelper } from '../types.js';
3
3
  export class DateHelperMySQL extends DateHelper {
4
4
  readTimestampString(date) {
5
5
  const parsedDate = new Date(date);
@@ -1,5 +1,5 @@
1
- import { DatabaseHelper } from '../types.js';
2
1
  import { parseISO } from 'date-fns';
2
+ import { DatabaseHelper } from '../types.js';
3
3
  export class DateHelper extends DatabaseHelper {
4
4
  parse(date) {
5
5
  // Date generated from NOW()
@@ -5,6 +5,7 @@ import { SchemaHelper } from '../types.js';
5
5
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
6
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
7
7
  constraintName(existingName: string): string;
8
+ changePrimaryKey(table: string, to: string | string[]): Promise<void>;
8
9
  getDatabaseSize(): Promise<number | null>;
9
10
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
10
11
  createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;