@directus/api 13.1.1 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/dist/__utils__/snapshots.js +9 -0
  2. package/dist/app.js +6 -4
  3. package/dist/auth/drivers/ldap.js +3 -2
  4. package/dist/auth/drivers/local.js +1 -1
  5. package/dist/auth/drivers/oauth2.js +1 -1
  6. package/dist/auth/drivers/openid.js +1 -1
  7. package/dist/auth/drivers/saml.js +1 -1
  8. package/dist/auth.js +1 -1
  9. package/dist/cli/index.js +7 -4
  10. package/dist/controllers/activity.js +1 -1
  11. package/dist/controllers/assets.js +2 -2
  12. package/dist/controllers/auth.js +1 -1
  13. package/dist/controllers/collections.js +1 -1
  14. package/dist/controllers/dashboards.js +1 -1
  15. package/dist/controllers/extensions.js +29 -16
  16. package/dist/controllers/fields.js +1 -1
  17. package/dist/controllers/files.js +1 -1
  18. package/dist/controllers/flows.js +1 -1
  19. package/dist/controllers/folders.js +1 -1
  20. package/dist/controllers/items.js +1 -1
  21. package/dist/controllers/not-found.js +1 -1
  22. package/dist/controllers/notifications.js +1 -1
  23. package/dist/controllers/operations.js +1 -1
  24. package/dist/controllers/panels.js +1 -1
  25. package/dist/controllers/permissions.js +1 -1
  26. package/dist/controllers/presets.js +1 -1
  27. package/dist/controllers/relations.js +1 -1
  28. package/dist/controllers/roles.js +1 -1
  29. package/dist/controllers/schema.js +1 -1
  30. package/dist/controllers/server.js +1 -1
  31. package/dist/controllers/settings.js +1 -1
  32. package/dist/controllers/shares.js +1 -1
  33. package/dist/controllers/translations.js +1 -1
  34. package/dist/controllers/users.js +1 -1
  35. package/dist/controllers/utils.js +37 -18
  36. package/dist/controllers/versions.d.ts +2 -0
  37. package/dist/controllers/versions.js +188 -0
  38. package/dist/controllers/webhooks.js +1 -1
  39. package/dist/database/errors/dialects/mssql.js +1 -1
  40. package/dist/database/errors/dialects/mysql.js +1 -1
  41. package/dist/database/errors/dialects/oracle.js +1 -1
  42. package/dist/database/errors/dialects/postgres.js +1 -1
  43. package/dist/database/errors/dialects/sqlite.js +1 -1
  44. package/dist/database/helpers/schema/dialects/mysql.js +1 -1
  45. package/dist/database/helpers/sequence/dialects/postgres.d.ts +5 -2
  46. package/dist/database/helpers/sequence/dialects/postgres.js +6 -3
  47. package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
  48. package/dist/database/migrations/20230823A-add-content-versioning.js +36 -0
  49. package/dist/database/migrations/20230927A-themes.d.ts +3 -0
  50. package/dist/database/migrations/20230927A-themes.js +49 -0
  51. package/dist/database/migrations/20231009A-update-csv-fields-to-text.d.ts +3 -0
  52. package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +44 -0
  53. package/dist/database/migrations/20231009B-update-panel-options.d.ts +3 -0
  54. package/dist/database/migrations/20231009B-update-panel-options.js +77 -0
  55. package/dist/database/migrations/20231010A-add-extensions.d.ts +3 -0
  56. package/dist/database/migrations/20231010A-add-extensions.js +9 -0
  57. package/dist/database/run-ast.js +2 -2
  58. package/dist/database/seeds/run.js +1 -1
  59. package/dist/database/system-data/collections/collections.yaml +6 -0
  60. package/dist/database/system-data/fields/activity.yaml +4 -4
  61. package/dist/database/system-data/fields/collections.yaml +19 -0
  62. package/dist/database/system-data/fields/extensions.yaml +10 -0
  63. package/dist/database/system-data/fields/revisions.yaml +3 -0
  64. package/dist/database/system-data/fields/settings.yaml +73 -17
  65. package/dist/database/system-data/fields/users.yaml +48 -12
  66. package/dist/database/system-data/fields/versions.yaml +38 -0
  67. package/dist/database/system-data/fields/webhooks.yaml +9 -9
  68. package/dist/database/system-data/relations/relations.yaml +88 -20
  69. package/dist/env.js +4 -0
  70. package/dist/extensions/index.d.ts +2 -0
  71. package/dist/extensions/index.js +9 -0
  72. package/dist/extensions/lib/get-extensions-settings.d.ts +7 -0
  73. package/dist/extensions/lib/get-extensions-settings.js +39 -0
  74. package/dist/extensions/lib/get-extensions.d.ts +1 -0
  75. package/dist/extensions/lib/get-extensions.js +11 -0
  76. package/dist/extensions/lib/get-shared-deps-mapping.d.ts +1 -0
  77. package/dist/extensions/lib/get-shared-deps-mapping.js +26 -0
  78. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +31 -0
  79. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.js +80 -0
  80. package/dist/extensions/lib/sandbox/generate-host-function-reference.d.ts +11 -0
  81. package/dist/extensions/lib/sandbox/generate-host-function-reference.js +28 -0
  82. package/dist/extensions/lib/sandbox/register/action.d.ts +6 -0
  83. package/dist/extensions/lib/sandbox/register/action.js +18 -0
  84. package/dist/extensions/lib/sandbox/register/call-reference.d.ts +5 -0
  85. package/dist/extensions/lib/sandbox/register/call-reference.js +20 -0
  86. package/dist/extensions/lib/sandbox/register/filter.d.ts +6 -0
  87. package/dist/extensions/lib/sandbox/register/filter.js +21 -0
  88. package/dist/extensions/lib/sandbox/register/index.d.ts +5 -0
  89. package/dist/extensions/lib/sandbox/register/index.js +5 -0
  90. package/dist/extensions/lib/sandbox/register/operation.d.ts +6 -0
  91. package/dist/extensions/lib/sandbox/register/operation.js +19 -0
  92. package/dist/extensions/lib/sandbox/register/route.d.ts +17 -0
  93. package/dist/extensions/lib/sandbox/register/route.js +44 -0
  94. package/dist/extensions/lib/sandbox/sdk/generators/index.d.ts +3 -0
  95. package/dist/extensions/lib/sandbox/sdk/generators/index.js +3 -0
  96. package/dist/extensions/lib/sandbox/sdk/generators/log.d.ts +3 -0
  97. package/dist/extensions/lib/sandbox/sdk/generators/log.js +11 -0
  98. package/dist/extensions/lib/sandbox/sdk/generators/request.d.ts +12 -0
  99. package/dist/extensions/lib/sandbox/sdk/generators/request.js +49 -0
  100. package/dist/extensions/lib/sandbox/sdk/generators/sleep.d.ts +3 -0
  101. package/dist/extensions/lib/sandbox/sdk/generators/sleep.js +11 -0
  102. package/dist/extensions/lib/sandbox/sdk/index.d.ts +2 -0
  103. package/dist/extensions/lib/sandbox/sdk/index.js +2 -0
  104. package/dist/extensions/lib/sandbox/sdk/instantiate.d.ts +11 -0
  105. package/dist/extensions/lib/sandbox/sdk/instantiate.js +28 -0
  106. package/dist/extensions/lib/sandbox/sdk/sdk.d.ts +20 -0
  107. package/dist/extensions/lib/sandbox/sdk/sdk.js +11 -0
  108. package/dist/extensions/lib/sandbox/sdk/utils/index.d.ts +1 -0
  109. package/dist/extensions/lib/sandbox/sdk/utils/index.js +1 -0
  110. package/dist/extensions/lib/sandbox/sdk/utils/wrap.d.ts +11 -0
  111. package/dist/extensions/lib/sandbox/sdk/utils/wrap.js +17 -0
  112. package/dist/extensions/lib/wrap-embeds.d.ts +4 -0
  113. package/dist/extensions/lib/wrap-embeds.js +8 -0
  114. package/dist/extensions/manager.d.ts +158 -0
  115. package/dist/extensions/manager.js +604 -0
  116. package/dist/extensions/types.d.ts +19 -0
  117. package/dist/flows.d.ts +2 -2
  118. package/dist/flows.js +7 -7
  119. package/dist/middleware/check-ip.js +1 -1
  120. package/dist/middleware/collection-exists.js +1 -1
  121. package/dist/middleware/error-handler.js +1 -1
  122. package/dist/middleware/graphql.js +1 -1
  123. package/dist/middleware/rate-limiter-global.js +1 -1
  124. package/dist/middleware/rate-limiter-ip.js +1 -1
  125. package/dist/middleware/respond.js +13 -1
  126. package/dist/middleware/validate-batch.js +1 -1
  127. package/dist/operations/condition/index.d.ts +1 -1
  128. package/dist/operations/condition/index.js +2 -1
  129. package/dist/operations/exec/index.d.ts +1 -1
  130. package/dist/operations/exec/index.js +1 -1
  131. package/dist/operations/item-create/index.d.ts +1 -1
  132. package/dist/operations/item-create/index.js +2 -1
  133. package/dist/operations/item-delete/index.d.ts +1 -1
  134. package/dist/operations/item-delete/index.js +2 -1
  135. package/dist/operations/item-read/index.d.ts +1 -1
  136. package/dist/operations/item-read/index.js +2 -1
  137. package/dist/operations/item-update/index.d.ts +1 -1
  138. package/dist/operations/item-update/index.js +2 -1
  139. package/dist/operations/json-web-token/index.d.ts +1 -1
  140. package/dist/operations/json-web-token/index.js +2 -1
  141. package/dist/operations/log/index.d.ts +1 -1
  142. package/dist/operations/log/index.js +2 -1
  143. package/dist/operations/mail/index.d.ts +1 -1
  144. package/dist/operations/mail/index.js +1 -1
  145. package/dist/operations/notification/index.d.ts +1 -1
  146. package/dist/operations/notification/index.js +2 -1
  147. package/dist/operations/request/index.d.ts +1 -1
  148. package/dist/operations/request/index.js +3 -2
  149. package/dist/operations/sleep/index.d.ts +1 -1
  150. package/dist/operations/sleep/index.js +1 -1
  151. package/dist/operations/transform/index.d.ts +1 -1
  152. package/dist/operations/transform/index.js +2 -1
  153. package/dist/operations/trigger/index.d.ts +1 -1
  154. package/dist/operations/trigger/index.js +2 -1
  155. package/dist/services/activity.js +1 -1
  156. package/dist/services/assets.d.ts +1 -1
  157. package/dist/services/assets.js +3 -3
  158. package/dist/services/authentication.js +2 -2
  159. package/dist/services/authorization.js +1 -1
  160. package/dist/services/collections.js +3 -3
  161. package/dist/services/extensions.d.ts +31 -0
  162. package/dist/services/extensions.js +121 -0
  163. package/dist/services/fields.d.ts +2 -2
  164. package/dist/services/fields.js +4 -4
  165. package/dist/services/files.d.ts +4 -1
  166. package/dist/services/files.js +5 -5
  167. package/dist/services/graphql/index.d.ts +1 -1
  168. package/dist/services/graphql/index.js +87 -24
  169. package/dist/services/graphql/subscription.js +3 -3
  170. package/dist/services/import-export/import-worker.d.ts +9 -0
  171. package/dist/services/import-export/import-worker.js +9 -0
  172. package/dist/services/{import-export.d.ts → import-export/index.d.ts} +2 -2
  173. package/dist/services/{import-export.js → import-export/index.js} +51 -42
  174. package/dist/services/index.d.ts +3 -1
  175. package/dist/services/index.js +3 -1
  176. package/dist/services/items.js +2 -2
  177. package/dist/services/mail/index.js +1 -1
  178. package/dist/services/meta.js +1 -1
  179. package/dist/services/payload.js +1 -1
  180. package/dist/services/permissions.d.ts +2 -2
  181. package/dist/services/permissions.js +1 -1
  182. package/dist/services/relations.js +1 -1
  183. package/dist/services/revisions.js +1 -1
  184. package/dist/services/roles.js +1 -1
  185. package/dist/services/schema.js +1 -1
  186. package/dist/services/server.js +3 -1
  187. package/dist/services/shares.js +1 -1
  188. package/dist/services/tfa.js +1 -1
  189. package/dist/services/translations.js +1 -1
  190. package/dist/services/users.js +4 -2
  191. package/dist/services/utils.d.ts +1 -0
  192. package/dist/services/utils.js +8 -2
  193. package/dist/services/versions.d.ts +21 -0
  194. package/dist/services/versions.js +232 -0
  195. package/dist/services/websocket.js +11 -1
  196. package/dist/types/collection.d.ts +1 -0
  197. package/dist/types/index.d.ts +0 -1
  198. package/dist/types/index.js +0 -1
  199. package/dist/utils/apply-query.d.ts +1 -1
  200. package/dist/utils/apply-query.js +31 -3
  201. package/dist/utils/delete-from-require-cache.d.ts +1 -0
  202. package/dist/utils/delete-from-require-cache.js +5 -0
  203. package/dist/utils/get-accountability-for-token.js +1 -1
  204. package/dist/utils/get-ast-from-query.js +1 -1
  205. package/dist/utils/get-column-path.js +1 -1
  206. package/dist/utils/get-column.js +1 -1
  207. package/dist/utils/get-default-value.d.ts +1 -2
  208. package/dist/utils/get-permissions.js +1 -1
  209. package/dist/utils/get-service.js +3 -1
  210. package/dist/utils/import-file-url.d.ts +5 -0
  211. package/dist/utils/import-file-url.js +6 -0
  212. package/dist/utils/job-queue.d.ts +2 -3
  213. package/dist/utils/jwt.js +1 -1
  214. package/dist/utils/redact-object.js +9 -3
  215. package/dist/utils/sanitize-query.js +3 -0
  216. package/dist/utils/transformations.d.ts +2 -1
  217. package/dist/utils/validate-diff.js +1 -1
  218. package/dist/utils/validate-keys.js +1 -1
  219. package/dist/utils/validate-query.js +2 -1
  220. package/dist/utils/validate-snapshot.js +1 -1
  221. package/dist/websocket/controllers/base.js +1 -1
  222. package/dist/websocket/controllers/index.d.ts +1 -1
  223. package/dist/websocket/controllers/index.js +0 -7
  224. package/dist/websocket/handlers/heartbeat.js +6 -1
  225. package/dist/websocket/handlers/subscribe.js +11 -16
  226. package/dist/websocket/utils/items.d.ts +4 -14
  227. package/dist/websocket/utils/items.js +59 -64
  228. package/dist/worker-pool.d.ts +2 -0
  229. package/dist/worker-pool.js +11 -0
  230. package/package.json +34 -31
  231. package/dist/errors/codes.d.ts +0 -29
  232. package/dist/errors/codes.js +0 -30
  233. package/dist/errors/contains-null-values.d.ts +0 -7
  234. package/dist/errors/contains-null-values.js +0 -4
  235. package/dist/errors/content-too-large.d.ts +0 -1
  236. package/dist/errors/content-too-large.js +0 -3
  237. package/dist/errors/forbidden.d.ts +0 -1
  238. package/dist/errors/forbidden.js +0 -3
  239. package/dist/errors/hit-rate-limit.d.ts +0 -6
  240. package/dist/errors/hit-rate-limit.js +0 -8
  241. package/dist/errors/illegal-asset-transformation.d.ts +0 -4
  242. package/dist/errors/illegal-asset-transformation.js +0 -3
  243. package/dist/errors/index.d.ts +0 -28
  244. package/dist/errors/index.js +0 -28
  245. package/dist/errors/invalid-credentials.d.ts +0 -1
  246. package/dist/errors/invalid-credentials.js +0 -3
  247. package/dist/errors/invalid-foreign-key.d.ts +0 -6
  248. package/dist/errors/invalid-foreign-key.js +0 -14
  249. package/dist/errors/invalid-ip.d.ts +0 -1
  250. package/dist/errors/invalid-ip.js +0 -3
  251. package/dist/errors/invalid-otp.d.ts +0 -1
  252. package/dist/errors/invalid-otp.js +0 -3
  253. package/dist/errors/invalid-payload.d.ts +0 -5
  254. package/dist/errors/invalid-payload.js +0 -4
  255. package/dist/errors/invalid-provider-config.d.ts +0 -5
  256. package/dist/errors/invalid-provider-config.js +0 -3
  257. package/dist/errors/invalid-provider.d.ts +0 -1
  258. package/dist/errors/invalid-provider.js +0 -3
  259. package/dist/errors/invalid-query.d.ts +0 -5
  260. package/dist/errors/invalid-query.js +0 -4
  261. package/dist/errors/invalid-token.d.ts +0 -1
  262. package/dist/errors/invalid-token.js +0 -3
  263. package/dist/errors/method-not-allowed.d.ts +0 -6
  264. package/dist/errors/method-not-allowed.js +0 -6
  265. package/dist/errors/not-null-violation.d.ts +0 -6
  266. package/dist/errors/not-null-violation.js +0 -14
  267. package/dist/errors/range-not-satisfiable.d.ts +0 -7
  268. package/dist/errors/range-not-satisfiable.js +0 -7
  269. package/dist/errors/record-not-unique.d.ts +0 -6
  270. package/dist/errors/record-not-unique.js +0 -14
  271. package/dist/errors/route-not-found.d.ts +0 -5
  272. package/dist/errors/route-not-found.js +0 -4
  273. package/dist/errors/service-unavailable.d.ts +0 -7
  274. package/dist/errors/service-unavailable.js +0 -4
  275. package/dist/errors/token-expired.d.ts +0 -1
  276. package/dist/errors/token-expired.js +0 -3
  277. package/dist/errors/unexpected-response.d.ts +0 -1
  278. package/dist/errors/unexpected-response.js +0 -3
  279. package/dist/errors/unprocessable-content.d.ts +0 -5
  280. package/dist/errors/unprocessable-content.js +0 -4
  281. package/dist/errors/unsupported-media-type.d.ts +0 -6
  282. package/dist/errors/unsupported-media-type.js +0 -4
  283. package/dist/errors/user-suspended.d.ts +0 -1
  284. package/dist/errors/user-suspended.js +0 -3
  285. package/dist/errors/value-out-of-range.d.ts +0 -6
  286. package/dist/errors/value-out-of-range.js +0 -14
  287. package/dist/errors/value-too-long.d.ts +0 -6
  288. package/dist/errors/value-too-long.js +0 -14
  289. package/dist/extensions.d.ts +0 -51
  290. package/dist/extensions.js +0 -487
  291. package/dist/types/files.d.ts +0 -29
  292. /package/dist/{types/files.js → extensions/types.js} +0 -0
@@ -6,8 +6,8 @@ import { cloneDeep, isEmpty } from 'lodash-es';
6
6
  import { performance } from 'perf_hooks';
7
7
  import getDatabase from '../database/index.js';
8
8
  import env from '../env.js';
9
- import { ForbiddenError } from '../errors/forbidden.js';
10
- import { InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '../errors/index.js';
9
+ import { ForbiddenError } from '@directus/errors';
10
+ import { InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
11
11
  import isUrlAllowed from '../utils/is-url-allowed.js';
12
12
  import { verifyJWT } from '../utils/jwt.js';
13
13
  import { stall } from '../utils/stall.js';
@@ -274,7 +274,9 @@ export class UsersService extends ItemsService {
274
274
  catch (err) {
275
275
  (opts || (opts = {})).preMutationError = err;
276
276
  }
277
+ // Manual constraint, see https://github.com/directus/directus/pull/19912
277
278
  await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
279
+ await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
278
280
  await super.deleteMany(keys, opts);
279
281
  return keys;
280
282
  }
@@ -10,4 +10,5 @@ export declare class UtilsService {
10
10
  item: PrimaryKey;
11
11
  to: PrimaryKey;
12
12
  }): Promise<void>;
13
+ clearCache(): Promise<void>;
13
14
  }
@@ -1,8 +1,8 @@
1
+ import { flushCaches, getCache } from '../cache.js';
1
2
  import getDatabase from '../database/index.js';
2
3
  import { systemCollectionRows } from '../database/system-data/collections/index.js';
3
4
  import emitter from '../emitter.js';
4
- import { ForbiddenError, InvalidPayloadError } from '../errors/index.js';
5
- import { getCache } from '../cache.js';
5
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
6
6
  import { shouldClearCache } from '../utils/should-clear-cache.js';
7
7
  export class UtilsService {
8
8
  knex;
@@ -112,4 +112,10 @@ export class UtilsService {
112
112
  accountability: this.accountability,
113
113
  });
114
114
  }
115
+ async clearCache() {
116
+ if (this.accountability?.admin !== true) {
117
+ throw new ForbiddenError();
118
+ }
119
+ return flushCaches(true);
120
+ }
115
121
  }
@@ -0,0 +1,21 @@
1
+ import type { Item, PrimaryKey, Query } from '@directus/types';
2
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
+ import { AuthorizationService } from './authorization.js';
4
+ import { ItemsService } from './items.js';
5
+ export declare class VersionsService extends ItemsService {
6
+ authorizationService: AuthorizationService;
7
+ constructor(options: AbstractServiceOptions);
8
+ private validateCreateData;
9
+ getMainItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
10
+ verifyHash(collection: string, item: PrimaryKey, hash: string): Promise<{
11
+ outdated: boolean;
12
+ mainHash: string;
13
+ }>;
14
+ getVersionSavesById(id: PrimaryKey): Promise<Partial<Item>[]>;
15
+ getVersionSaves(key: string, collection: string, item: string | undefined): Promise<Partial<Item>[] | null>;
16
+ createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
17
+ createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
18
+ updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
19
+ save(key: PrimaryKey, data: Partial<Item>): Promise<Partial<Item>>;
20
+ promote(version: PrimaryKey, mainHash: string, fields?: string[]): Promise<import("../types/items.js").PrimaryKey>;
21
+ }
@@ -0,0 +1,232 @@
1
+ import { Action } from '@directus/constants';
2
+ import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
3
+ import Joi from 'joi';
4
+ import { assign, pick } from 'lodash-es';
5
+ import objectHash from 'object-hash';
6
+ import getDatabase from '../database/index.js';
7
+ import emitter from '../emitter.js';
8
+ import { ActivityService } from './activity.js';
9
+ import { AuthorizationService } from './authorization.js';
10
+ import { ItemsService } from './items.js';
11
+ import { PayloadService } from './payload.js';
12
+ import { RevisionsService } from './revisions.js';
13
+ export class VersionsService extends ItemsService {
14
+ authorizationService;
15
+ constructor(options) {
16
+ super('directus_versions', options);
17
+ this.authorizationService = new AuthorizationService({
18
+ accountability: this.accountability,
19
+ knex: this.knex,
20
+ schema: this.schema,
21
+ });
22
+ }
23
+ async validateCreateData(data) {
24
+ if (!data['key'])
25
+ throw new InvalidPayloadError({ reason: `"key" is required` });
26
+ // Reserves the "main" version key for the version query parameter
27
+ if (data['key'] === 'main')
28
+ throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
29
+ if (!data['collection']) {
30
+ throw new InvalidPayloadError({ reason: `"collection" is required` });
31
+ }
32
+ if (!data['item'])
33
+ throw new InvalidPayloadError({ reason: `"item" is required` });
34
+ const { CollectionsService } = await import('./collections.js');
35
+ const collectionsService = new CollectionsService({
36
+ accountability: null,
37
+ knex: this.knex,
38
+ schema: this.schema,
39
+ });
40
+ const existingCollection = await collectionsService.readOne(data['collection']);
41
+ if (!existingCollection.meta?.versioning) {
42
+ throw new UnprocessableContentError({
43
+ reason: `Content Versioning is not enabled for collection "${data['collection']}"`,
44
+ });
45
+ }
46
+ const existingVersions = await super.readByQuery({
47
+ aggregate: { count: ['*'] },
48
+ filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
49
+ });
50
+ if (existingVersions[0]['count'] > 0) {
51
+ throw new UnprocessableContentError({
52
+ reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
53
+ });
54
+ }
55
+ // will throw an error if the accountability does not have permission to read the item
56
+ await this.authorizationService.checkAccess('read', data['collection'], data['item']);
57
+ }
58
+ async getMainItem(collection, item, query) {
59
+ // will throw an error if the accountability does not have permission to read the item
60
+ await this.authorizationService.checkAccess('read', collection, item);
61
+ const itemsService = new ItemsService(collection, {
62
+ knex: this.knex,
63
+ accountability: this.accountability,
64
+ schema: this.schema,
65
+ });
66
+ return await itemsService.readOne(item, query);
67
+ }
68
+ async verifyHash(collection, item, hash) {
69
+ const mainItem = await this.getMainItem(collection, item);
70
+ const mainHash = objectHash(mainItem);
71
+ return { outdated: hash !== mainHash, mainHash };
72
+ }
73
+ async getVersionSavesById(id) {
74
+ const revisionsService = new RevisionsService({
75
+ knex: this.knex,
76
+ schema: this.schema,
77
+ });
78
+ const result = await revisionsService.readByQuery({
79
+ filter: { version: { _eq: id } },
80
+ });
81
+ return result.map((revision) => revision['delta']);
82
+ }
83
+ async getVersionSaves(key, collection, item) {
84
+ const filter = {
85
+ key: { _eq: key },
86
+ collection: { _eq: collection },
87
+ };
88
+ if (item) {
89
+ filter['item'] = { _eq: item };
90
+ }
91
+ const versions = await this.readByQuery({ filter });
92
+ if (!versions?.[0])
93
+ return null;
94
+ const saves = await this.getVersionSavesById(versions[0]['id']);
95
+ return saves;
96
+ }
97
+ async createOne(data, opts) {
98
+ await this.validateCreateData(data);
99
+ const mainItem = await this.getMainItem(data['collection'], data['item']);
100
+ data['hash'] = objectHash(mainItem);
101
+ return super.createOne(data, opts);
102
+ }
103
+ async createMany(data, opts) {
104
+ if (!Array.isArray(data)) {
105
+ throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
106
+ }
107
+ const keyCombos = new Set();
108
+ for (const item of data) {
109
+ await this.validateCreateData(item);
110
+ const keyCombo = `${item['key']}-${item['collection']}-${item['item']}`;
111
+ if (keyCombos.has(keyCombo)) {
112
+ throw new UnprocessableContentError({
113
+ reason: `Cannot create multiple versions on "${item['item']}" in collection "${item['collection']}" with the same key "${item['key']}"`,
114
+ });
115
+ }
116
+ keyCombos.add(keyCombo);
117
+ const mainItem = await this.getMainItem(item['collection'], item['item']);
118
+ item['hash'] = objectHash(mainItem);
119
+ }
120
+ return super.createMany(data, opts);
121
+ }
122
+ async updateMany(keys, data, opts) {
123
+ // Only allow updates on "key" and "name" fields
124
+ const versionUpdateSchema = Joi.object({
125
+ key: Joi.string(),
126
+ name: Joi.string().allow(null).optional(),
127
+ });
128
+ const { error } = versionUpdateSchema.validate(data);
129
+ if (error)
130
+ throw new InvalidPayloadError({ reason: error.message });
131
+ if ('key' in data) {
132
+ // Reserves the "main" version key for the version query parameter
133
+ if (data['key'] === 'main')
134
+ throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
135
+ const keyCombos = new Set();
136
+ for (const pk of keys) {
137
+ const { collection, item } = await this.readOne(pk, { fields: ['collection', 'item'] });
138
+ const keyCombo = `${data['key']}-${collection}-${item}`;
139
+ if (keyCombos.has(keyCombo)) {
140
+ throw new UnprocessableContentError({
141
+ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${data['key']}"`,
142
+ });
143
+ }
144
+ keyCombos.add(keyCombo);
145
+ const existingVersions = await super.readByQuery({
146
+ aggregate: { count: ['*'] },
147
+ filter: { id: { _neq: pk }, key: { _eq: data['key'] }, collection: { _eq: collection }, item: { _eq: item } },
148
+ });
149
+ if (existingVersions[0]['count'] > 0) {
150
+ throw new UnprocessableContentError({
151
+ reason: `Version "${data['key']}" already exists for item "${item}" in collection "${collection}"`,
152
+ });
153
+ }
154
+ }
155
+ }
156
+ return super.updateMany(keys, data, opts);
157
+ }
158
+ async save(key, data) {
159
+ const version = await super.readOne(key);
160
+ const payloadService = new PayloadService(this.collection, {
161
+ accountability: this.accountability,
162
+ knex: this.knex,
163
+ schema: this.schema,
164
+ });
165
+ const activityService = new ActivityService({
166
+ knex: this.knex,
167
+ schema: this.schema,
168
+ });
169
+ const revisionsService = new RevisionsService({
170
+ knex: this.knex,
171
+ schema: this.schema,
172
+ });
173
+ const activity = await activityService.createOne({
174
+ action: Action.VERSION_SAVE,
175
+ user: this.accountability?.user ?? null,
176
+ collection: version['collection'],
177
+ ip: this.accountability?.ip ?? null,
178
+ user_agent: this.accountability?.userAgent ?? null,
179
+ origin: this.accountability?.origin ?? null,
180
+ item: version['item'],
181
+ });
182
+ const revisionDelta = await payloadService.prepareDelta(data);
183
+ await revisionsService.createOne({
184
+ activity,
185
+ version: key,
186
+ collection: version['collection'],
187
+ item: version['item'],
188
+ data: revisionDelta,
189
+ delta: revisionDelta,
190
+ });
191
+ return data;
192
+ }
193
+ async promote(version, mainHash, fields) {
194
+ const { id, collection, item } = (await this.readOne(version));
195
+ // will throw an error if the accountability does not have permission to update the item
196
+ await this.authorizationService.checkAccess('update', collection, item);
197
+ const { outdated } = await this.verifyHash(collection, item, mainHash);
198
+ if (outdated) {
199
+ throw new UnprocessableContentError({
200
+ reason: `Main item has changed since this version was last updated`,
201
+ });
202
+ }
203
+ const saves = await this.getVersionSavesById(id);
204
+ const versionResult = assign({}, ...saves);
205
+ const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
206
+ const itemsService = new ItemsService(collection, {
207
+ accountability: this.accountability,
208
+ schema: this.schema,
209
+ });
210
+ const payloadAfterHooks = await emitter.emitFilter(['items.promote', `${collection}.items.promote`], payloadToUpdate, {
211
+ collection,
212
+ item,
213
+ version,
214
+ }, {
215
+ database: getDatabase(),
216
+ schema: this.schema,
217
+ accountability: this.accountability,
218
+ });
219
+ const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks);
220
+ emitter.emitAction(['items.promote', `${collection}.items.promote`], {
221
+ payload: payloadAfterHooks,
222
+ collection,
223
+ item: updatedItemKey,
224
+ version,
225
+ }, {
226
+ database: getDatabase(),
227
+ schema: this.schema,
228
+ accountability: this.accountability,
229
+ });
230
+ return updatedItemKey;
231
+ }
232
+ }
@@ -1,9 +1,19 @@
1
1
  import { getWebSocketController } from '../websocket/controllers/index.js';
2
+ import { ServiceUnavailableError } from '@directus/errors';
3
+ import { toBoolean } from '../utils/to-boolean.js';
2
4
  import emitter from '../emitter.js';
5
+ import env from '../env.js';
3
6
  export class WebSocketService {
4
7
  controller;
5
8
  constructor() {
6
- this.controller = getWebSocketController();
9
+ if (!toBoolean(env['WEBSOCKETS_ENABLED']) || !toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
10
+ throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is disabled' });
11
+ }
12
+ const controller = getWebSocketController();
13
+ if (!controller) {
14
+ throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is not initialized' });
15
+ }
16
+ this.controller = controller;
7
17
  }
8
18
  on(event, callback) {
9
19
  emitter.onAction('websocket.' + event, callback);
@@ -7,6 +7,7 @@ export type CollectionMeta = {
7
7
  singleton: boolean;
8
8
  icon: string | null;
9
9
  translations: Record<string, string>;
10
+ versioning: boolean;
10
11
  item_duplication_fields: string[] | null;
11
12
  accountability: 'all' | 'accountability' | null;
12
13
  group: string | null;
@@ -4,7 +4,6 @@ export * from './auth.js';
4
4
  export * from './collection.js';
5
5
  export * from './database.js';
6
6
  export * from './events.js';
7
- export * from './files.js';
8
7
  export * from './graphql.js';
9
8
  export * from './items.js';
10
9
  export * from './meta.js';
@@ -4,7 +4,6 @@ export * from './auth.js';
4
4
  export * from './collection.js';
5
5
  export * from './database.js';
6
6
  export * from './events.js';
7
- export * from './files.js';
8
7
  export * from './graphql.js';
9
8
  export * from './items.js';
10
9
  export * from './meta.js';
@@ -18,7 +18,7 @@ export type ColumnSortRecord = {
18
18
  order: 'asc' | 'desc';
19
19
  column: string;
20
20
  };
21
- export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootSort: string[], collection: string, aliasMap: AliasMap, returnRecords?: boolean): {
21
+ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, query: Query, collection: string, aliasMap: AliasMap, returnRecords?: boolean): {
22
22
  sortRecords: {
23
23
  order: "asc" | "desc";
24
24
  column: any;
@@ -3,7 +3,7 @@ import { clone, isPlainObject } from 'lodash-es';
3
3
  import { customAlphabet } from 'nanoid/non-secure';
4
4
  import validate from 'uuid-validate';
5
5
  import { getHelpers } from '../database/helpers/index.js';
6
- import { InvalidQueryError } from '../errors/index.js';
6
+ import { InvalidQueryError } from '@directus/errors';
7
7
  import { getColumnPath } from './get-column-path.js';
8
8
  import { getColumn } from './get-column.js';
9
9
  import { getRelationInfo } from './get-relation-info.js';
@@ -24,7 +24,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
24
24
  applyOffset(knex, dbQuery, query.limit * (query.page - 1));
25
25
  }
26
26
  if (query.sort && !options?.isInnerQuery && !options?.hasMultiRelationalSort) {
27
- const sortResult = applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
27
+ const sortResult = applySort(knex, schema, dbQuery, query, collection, aliasMap);
28
28
  if (!hasJoins) {
29
29
  hasJoins = sortResult.hasJoins;
30
30
  }
@@ -128,7 +128,9 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
128
128
  }
129
129
  }
130
130
  }
131
- export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMap, returnRecords = false) {
131
+ export function applySort(knex, schema, rootQuery, query, collection, aliasMap, returnRecords = false) {
132
+ const rootSort = query.sort;
133
+ const aggregate = query?.aggregate;
132
134
  const relations = schema.relations;
133
135
  let hasJoins = false;
134
136
  let hasMultiRelationalSort = false;
@@ -141,6 +143,32 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
141
143
  if (column[0].startsWith('-')) {
142
144
  column[0] = column[0].substring(1);
143
145
  }
146
+ // Is the column name one of the aggregate functions used in the query if there is any?
147
+ if (Object.keys(aggregate ?? {}).includes(column[0])) {
148
+ // If so, return the column name without the order prefix
149
+ const operation = column[0];
150
+ // Get the field for the aggregate function
151
+ const field = column[1];
152
+ // If the operation is countAll there is no field.
153
+ if (operation === 'countAll') {
154
+ return {
155
+ order,
156
+ column: 'countAll',
157
+ };
158
+ }
159
+ // If the operation is a root count there is no field.
160
+ if (operation === 'count' && (field === '*' || !field)) {
161
+ return {
162
+ order,
163
+ column: 'count',
164
+ };
165
+ }
166
+ // Return the column name with the operation and field name
167
+ return {
168
+ order,
169
+ column: returnRecords ? column[0] : `${operation}->${field}`,
170
+ };
171
+ }
144
172
  if (column.length === 1) {
145
173
  const pathRoot = column[0].split(':')[0];
146
174
  const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
@@ -0,0 +1 @@
1
+ export declare function deleteFromRequireCache(modulePath: string): void;
@@ -0,0 +1,5 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export function deleteFromRequireCache(modulePath) {
4
+ delete require.cache[require.resolve(modulePath)];
5
+ }
@@ -1,6 +1,6 @@
1
1
  import getDatabase from '../database/index.js';
2
2
  import env from '../env.js';
3
- import { InvalidCredentialsError } from '../errors/index.js';
3
+ import { InvalidCredentialsError } from '@directus/errors';
4
4
  import isDirectusJWT from './is-directus-jwt.js';
5
5
  import { verifyAccessJWT } from './jwt.js';
6
6
  export async function getAccountabilityForToken(token, accountability) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { REGEX_BETWEEN_PARENS } from '@directus/constants';
5
5
  import { cloneDeep, isEmpty, mapKeys, omitBy, uniq } from 'lodash-es';
6
- import { getRelationType } from '../utils/get-relation-type.js';
6
+ import { getRelationType } from './get-relation-type.js';
7
7
  export default async function getASTFromQuery(collection, query, schema, options) {
8
8
  query = cloneDeep(query);
9
9
  const accountability = options?.accountability;
@@ -1,4 +1,4 @@
1
- import { InvalidQueryError } from '../errors/index.js';
1
+ import { InvalidQueryError } from '@directus/errors';
2
2
  import { getRelationInfo } from './get-relation-info.js';
3
3
  /**
4
4
  * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
@@ -1,7 +1,7 @@
1
1
  import { REGEX_BETWEEN_PARENS } from '@directus/constants';
2
2
  import { getFunctionsForType } from '@directus/utils';
3
3
  import { getFunctions } from '../database/helpers/index.js';
4
- import { InvalidQueryError } from '../errors/index.js';
4
+ import { InvalidQueryError } from '@directus/errors';
5
5
  import { applyFunctionToColumnName } from './apply-function-to-column-name.js';
6
6
  /**
7
7
  * Return column prefixed by table. If column includes functions (like `year(date_created)`), the
@@ -1,5 +1,4 @@
1
- import type { SchemaOverview } from '@directus/schema/types/overview';
2
- import type { Column } from '@directus/schema';
1
+ import type { Column, SchemaOverview } from '@directus/schema';
3
2
  import type { FieldMeta } from '@directus/types';
4
3
  export default function getDefaultValue(column: SchemaOverview[string]['columns'][string] | Column, field?: {
5
4
  special?: FieldMeta['special'];
@@ -8,7 +8,7 @@ import env from '../env.js';
8
8
  import logger from '../logger.js';
9
9
  import { RolesService } from '../services/roles.js';
10
10
  import { UsersService } from '../services/users.js';
11
- import { mergePermissions } from '../utils/merge-permissions.js';
11
+ import { mergePermissions } from './merge-permissions.js';
12
12
  import { mergePermissionsForShare } from './merge-permissions-for-share.js';
13
13
  export async function getPermissions(accountability, schema) {
14
14
  const database = getDatabase();
@@ -1,4 +1,4 @@
1
- import { ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, UsersService, WebhooksService, } from '../services/index.js';
1
+ import { ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
2
2
  /**
3
3
  * Select the correct service for the given collection. This allows the individual services to run
4
4
  * their custom checks (f.e. it allows UsersService to prevent updating TFA secret from outside)
@@ -43,6 +43,8 @@ export function getService(collection, opts) {
43
43
  return new UsersService(opts);
44
44
  case 'directus_webhooks':
45
45
  return new WebhooksService(opts);
46
+ case 'directus_versions':
47
+ return new VersionsService(opts);
46
48
  default:
47
49
  return new ItemsService(collection, opts);
48
50
  }
@@ -0,0 +1,5 @@
1
+ interface ImportOptions {
2
+ fresh?: boolean;
3
+ }
4
+ export declare function importFileUrl(url: string, root: string, options?: ImportOptions): Promise<any>;
5
+ export {};
@@ -0,0 +1,6 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { pathToRelativeUrl } from '@directus/utils/node';
4
+ export function importFileUrl(url, root, options = {}) {
5
+ return import(`./${pathToRelativeUrl(url, dirname(fileURLToPath(root)))}${options.fresh ? `?t=${Date.now()}` : ''}`);
6
+ }
@@ -1,9 +1,8 @@
1
- type Job = () => Promise<void> | void;
1
+ import type { PromiseCallback } from '@directus/types';
2
2
  export declare class JobQueue {
3
3
  private running;
4
4
  private jobs;
5
5
  constructor();
6
- enqueue(job: Job): void;
6
+ enqueue(job: PromiseCallback): void;
7
7
  private run;
8
8
  }
9
- export {};
package/dist/utils/jwt.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import jwt from 'jsonwebtoken';
2
- import { InvalidTokenError, ServiceUnavailableError, TokenExpiredError } from '../errors/index.js';
2
+ import { InvalidTokenError, ServiceUnavailableError, TokenExpiredError } from '@directus/errors';
3
3
  export function verifyJWT(token, secret) {
4
4
  let payload;
5
5
  try {
@@ -12,7 +12,6 @@ import { isObject } from '@directus/utils';
12
12
  export function redactObject(input, redact, replacement) {
13
13
  const wildcardChars = ['*', '**'];
14
14
  const clone = JSON.parse(JSON.stringify(input, getReplacer(replacement, redact.values)));
15
- const visited = new WeakSet();
16
15
  if (redact.keys) {
17
16
  traverse(clone, redact.keys);
18
17
  }
@@ -21,7 +20,6 @@ export function redactObject(input, redact, replacement) {
21
20
  if (checkKeyPaths.length === 0) {
22
21
  return;
23
22
  }
24
- visited.add(object);
25
23
  const REDACTED_TEXT = replacement();
26
24
  const globalCheckPaths = [];
27
25
  for (const key of Object.keys(object)) {
@@ -73,7 +71,7 @@ export function redactObject(input, redact, replacement) {
73
71
  }
74
72
  }
75
73
  const value = object[key];
76
- if (isObject(value) && !visited.has(value)) {
74
+ if (isObject(value)) {
77
75
  traverse(value, [...globalCheckPaths, ...localCheckPaths]);
78
76
  }
79
77
  }
@@ -86,7 +84,15 @@ export function getReplacer(replacement, values) {
86
84
  const filteredValues = values
87
85
  ? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
88
86
  : [];
87
+ const seen = new WeakSet();
89
88
  return (_key, value) => {
89
+ // Skip circular values
90
+ if (isObject(value)) {
91
+ if (seen.has(value)) {
92
+ return;
93
+ }
94
+ seen.add(value);
95
+ }
90
96
  if (value instanceof Error) {
91
97
  return {
92
98
  name: value.name,
@@ -46,6 +46,9 @@ export function sanitizeQuery(rawQuery, accountability) {
46
46
  if (rawQuery['search'] && typeof rawQuery['search'] === 'string') {
47
47
  query.search = rawQuery['search'];
48
48
  }
49
+ if (rawQuery['version']) {
50
+ query.version = rawQuery['version'];
51
+ }
49
52
  if (rawQuery['export']) {
50
53
  query.export = rawQuery['export'];
51
54
  }
@@ -1,4 +1,5 @@
1
- import type { File, Transformation, TransformationSet } from '../types/index.js';
1
+ import type { File } from '@directus/types';
2
+ import type { Transformation, TransformationSet } from '../types/index.js';
2
3
  export declare function resolvePreset({ transformationParams, acceptFormat }: TransformationSet, file: File): Transformation[];
3
4
  /**
4
5
  * Try to extract a file format from an array of `Transformation`'s.
@@ -1,5 +1,5 @@
1
1
  import Joi from 'joi';
2
- import { InvalidPayloadError } from '../errors/index.js';
2
+ import { InvalidPayloadError } from '@directus/errors';
3
3
  import { DiffKind } from '../types/snapshot.js';
4
4
  const deepDiffSchema = Joi.object({
5
5
  kind: Joi.string()
@@ -1,4 +1,4 @@
1
- import { ForbiddenError } from '../errors/index.js';
1
+ import { ForbiddenError } from '@directus/errors';
2
2
  import validateUUID from 'uuid-validate';
3
3
  /**
4
4
  * Validate keys based on its type
@@ -2,7 +2,7 @@ import Joi from 'joi';
2
2
  import { isPlainObject, uniq } from 'lodash-es';
3
3
  import { stringify } from 'wellknown';
4
4
  import env from '../env.js';
5
- import { InvalidQueryError } from '../errors/index.js';
5
+ import { InvalidQueryError } from '@directus/errors';
6
6
  import { calculateFieldDepth } from './calculate-field-depth.js';
7
7
  const querySchema = Joi.object({
8
8
  fields: Joi.array().items(Joi.string()),
@@ -17,6 +17,7 @@ const querySchema = Joi.object({
17
17
  meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
18
18
  search: Joi.string(),
19
19
  export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
20
+ version: Joi.string(),
20
21
  aggregate: Joi.object(),
21
22
  deep: Joi.object(),
22
23
  alias: Joi.object(),
@@ -2,7 +2,7 @@ import { TYPES } from '@directus/constants';
2
2
  import Joi from 'joi';
3
3
  import { ALIAS_TYPES } from '../constants.js';
4
4
  import { getDatabaseClient } from '../database/index.js';
5
- import { InvalidPayloadError } from '../errors/index.js';
5
+ import { InvalidPayloadError } from '@directus/errors';
6
6
  import { DatabaseClients } from '../types/index.js';
7
7
  import { version as currentDirectusVersion } from './package.js';
8
8
  const snapshotJoiSchema = Joi.object({