@directus/api 32.1.1 → 32.2.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 (165) hide show
  1. package/dist/ai/chat/constants/system-prompt.d.ts +1 -0
  2. package/dist/ai/chat/constants/system-prompt.js +51 -0
  3. package/dist/ai/chat/controllers/chat.post.d.ts +2 -0
  4. package/dist/ai/chat/controllers/chat.post.js +47 -0
  5. package/dist/ai/chat/lib/create-ui-stream.d.ts +15 -0
  6. package/dist/ai/chat/lib/create-ui-stream.js +42 -0
  7. package/dist/ai/chat/middleware/load-settings.d.ts +2 -0
  8. package/dist/ai/chat/middleware/load-settings.js +18 -0
  9. package/dist/ai/chat/models/chat-request.d.ts +34 -0
  10. package/dist/ai/chat/models/chat-request.js +26 -0
  11. package/dist/ai/chat/models/providers.d.ts +9 -0
  12. package/dist/ai/chat/models/providers.js +9 -0
  13. package/dist/ai/chat/router.d.ts +1 -0
  14. package/dist/ai/chat/router.js +5 -0
  15. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.d.ts +9 -0
  16. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +38 -0
  17. package/dist/ai/chat/utils/fix-error-tool-calls.d.ts +12 -0
  18. package/dist/ai/chat/utils/fix-error-tool-calls.js +30 -0
  19. package/dist/ai/chat/utils/parse-json-schema-7.d.ts +13 -0
  20. package/dist/ai/chat/utils/parse-json-schema-7.js +75 -0
  21. package/dist/{mcp → ai/mcp}/server.d.ts +13 -16
  22. package/dist/{mcp → ai/mcp}/server.js +4 -13
  23. package/dist/ai/mcp/types.d.ts +15 -0
  24. package/dist/{mcp/tools/assets.js → ai/tools/assets/index.js} +8 -5
  25. package/dist/{mcp/tools/collections.js → ai/tools/collections/index.js} +7 -4
  26. package/dist/{mcp/tools/fields.js → ai/tools/fields/index.js} +12 -9
  27. package/dist/{mcp/tools/files.js → ai/tools/files/index.js} +11 -5
  28. package/dist/{mcp/tools/flows.js → ai/tools/flows/index.js} +11 -5
  29. package/dist/{mcp/tools/folders.js → ai/tools/folders/index.js} +12 -5
  30. package/dist/ai/tools/index.d.ts +15 -0
  31. package/dist/ai/tools/index.js +29 -0
  32. package/dist/{mcp/tools/items.js → ai/tools/items/index.js} +13 -6
  33. package/dist/{mcp/tools/prompts/items.md → ai/tools/items/prompt.md} +19 -15
  34. package/dist/{mcp/tools/operations.d.ts → ai/tools/operations/index.d.ts} +46 -0
  35. package/dist/{mcp/tools/operations.js → ai/tools/operations/index.js} +12 -5
  36. package/dist/{mcp/tools/relations.js → ai/tools/relations/index.js} +7 -4
  37. package/dist/{mcp/tools/schema.d.ts → ai/tools/schema/index.d.ts} +1 -1
  38. package/dist/{mcp/tools/schema.js → ai/tools/schema/index.js} +9 -6
  39. package/dist/{mcp/tools/system.js → ai/tools/system/index.js} +7 -4
  40. package/dist/{mcp/tools/trigger-flow.js → ai/tools/trigger-flow/index.js} +8 -5
  41. package/dist/{mcp → ai/tools}/types.d.ts +1 -17
  42. package/dist/ai/tools/utils.d.ts +9 -0
  43. package/dist/ai/tools/utils.js +17 -0
  44. package/dist/app.js +5 -0
  45. package/dist/auth/drivers/saml.js +5 -2
  46. package/dist/controllers/assets.js +39 -2
  47. package/dist/controllers/mcp.js +1 -1
  48. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  49. package/dist/database/migrations/20251103A-add-ai-settings.d.ts +3 -0
  50. package/dist/database/migrations/20251103A-add-ai-settings.js +14 -0
  51. package/dist/database/run-ast/run-ast.js +1 -1
  52. package/dist/extensions/lib/installation/manager.js +5 -9
  53. package/dist/extensions/lib/sync/status.d.ts +11 -0
  54. package/dist/extensions/lib/sync/status.js +34 -0
  55. package/dist/extensions/lib/sync/sync.d.ts +6 -0
  56. package/dist/extensions/lib/sync/sync.js +90 -0
  57. package/dist/extensions/lib/sync/tracker.d.ts +18 -0
  58. package/dist/extensions/lib/sync/tracker.js +71 -0
  59. package/dist/extensions/lib/sync/utils.d.ts +24 -0
  60. package/dist/extensions/lib/sync/utils.js +62 -0
  61. package/dist/extensions/manager.d.ts +8 -4
  62. package/dist/extensions/manager.js +30 -13
  63. package/dist/middleware/respond.js +2 -2
  64. package/dist/permissions/lib/fetch-policies.d.ts +1 -1
  65. package/dist/permissions/lib/fetch-roles-tree.d.ts +6 -3
  66. package/dist/permissions/lib/fetch-roles-tree.js +5 -27
  67. package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +9 -7
  68. package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +17 -9
  69. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +1 -1
  70. package/dist/permissions/utils/fetch-raw-permissions.d.ts +1 -1
  71. package/dist/permissions/utils/fetch-share-info.d.ts +1 -1
  72. package/dist/permissions/utils/fetch-share-info.js +1 -1
  73. package/dist/permissions/utils/filter-policies-by-ip.js +1 -1
  74. package/dist/permissions/utils/get-permissions-for-share.js +8 -8
  75. package/dist/permissions/utils/with-cache.d.ts +8 -6
  76. package/dist/permissions/utils/with-cache.js +12 -10
  77. package/dist/request/is-denied-ip.js +2 -2
  78. package/dist/services/assets/name-deduper.d.ts +7 -0
  79. package/dist/services/assets/name-deduper.js +23 -0
  80. package/dist/services/assets.d.ts +15 -2
  81. package/dist/services/assets.js +98 -5
  82. package/dist/services/authentication.js +4 -4
  83. package/dist/services/comments.js +2 -2
  84. package/dist/services/extensions.js +4 -0
  85. package/dist/services/folders.d.ts +27 -2
  86. package/dist/services/folders.js +75 -0
  87. package/dist/services/import-export.d.ts +1 -1
  88. package/dist/services/import-export.js +4 -5
  89. package/dist/services/notifications.js +2 -2
  90. package/dist/services/payload.js +20 -0
  91. package/dist/services/roles.js +2 -2
  92. package/dist/services/tus/server.js +3 -3
  93. package/dist/telemetry/utils/get-settings.d.ts +15 -0
  94. package/dist/telemetry/utils/get-settings.js +13 -1
  95. package/dist/test-utils/README.md +95 -24
  96. package/dist/test-utils/cache.d.ts +2 -2
  97. package/dist/test-utils/cache.js +2 -2
  98. package/dist/test-utils/{fields-service.d.ts → services/fields-service.d.ts} +1 -1
  99. package/dist/test-utils/{fields-service.js → services/fields-service.js} +3 -2
  100. package/dist/test-utils/services/files-service.d.ts +28 -0
  101. package/dist/test-utils/services/files-service.js +34 -0
  102. package/dist/test-utils/services/folders-service.d.ts +28 -0
  103. package/dist/test-utils/services/folders-service.js +33 -0
  104. package/dist/utils/encrypt.d.ts +2 -0
  105. package/dist/utils/encrypt.js +64 -0
  106. package/dist/utils/get-accountability-for-role.js +2 -2
  107. package/dist/utils/get-accountability-for-token.js +4 -4
  108. package/dist/utils/get-cache-key.js +2 -2
  109. package/dist/utils/require-text.d.ts +1 -0
  110. package/dist/utils/require-text.js +4 -0
  111. package/dist/utils/require-yaml.js +2 -2
  112. package/package.json +32 -26
  113. package/dist/extensions/lib/sync-extensions.d.ts +0 -3
  114. package/dist/extensions/lib/sync-extensions.js +0 -70
  115. package/dist/extensions/lib/sync-status.d.ts +0 -10
  116. package/dist/extensions/lib/sync-status.js +0 -27
  117. package/dist/mcp/tools/index.d.ts +0 -15
  118. package/dist/mcp/tools/index.js +0 -29
  119. package/dist/mcp/tools/prompts/index.d.ts +0 -16
  120. package/dist/mcp/tools/prompts/index.js +0 -19
  121. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
  122. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
  123. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
  124. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
  125. package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
  126. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
  127. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
  128. package/dist/utils/get-date-formatted.d.ts +0 -1
  129. package/dist/utils/get-date-formatted.js +0 -10
  130. package/dist/utils/ip-in-networks.d.ts +0 -6
  131. package/dist/utils/ip-in-networks.js +0 -13
  132. /package/dist/{mcp → ai/mcp}/index.d.ts +0 -0
  133. /package/dist/{mcp → ai/mcp}/index.js +0 -0
  134. /package/dist/{mcp → ai/mcp}/transport.d.ts +0 -0
  135. /package/dist/{mcp → ai/mcp}/transport.js +0 -0
  136. /package/dist/{mcp → ai/mcp}/types.js +0 -0
  137. /package/dist/{mcp/tools/assets.d.ts → ai/tools/assets/index.d.ts} +0 -0
  138. /package/dist/{mcp/tools/prompts/assets.md → ai/tools/assets/prompt.md} +0 -0
  139. /package/dist/{mcp/tools/collections.d.ts → ai/tools/collections/index.d.ts} +0 -0
  140. /package/dist/{mcp/tools/prompts/collections.md → ai/tools/collections/prompt.md} +0 -0
  141. /package/dist/{mcp/define.d.ts → ai/tools/define-tool.d.ts} +0 -0
  142. /package/dist/{mcp/define.js → ai/tools/define-tool.js} +0 -0
  143. /package/dist/{mcp/tools/fields.d.ts → ai/tools/fields/index.d.ts} +0 -0
  144. /package/dist/{mcp/tools/prompts/fields.md → ai/tools/fields/prompt.md} +0 -0
  145. /package/dist/{mcp/tools/files.d.ts → ai/tools/files/index.d.ts} +0 -0
  146. /package/dist/{mcp/tools/prompts/files.md → ai/tools/files/prompt.md} +0 -0
  147. /package/dist/{mcp/tools/flows.d.ts → ai/tools/flows/index.d.ts} +0 -0
  148. /package/dist/{mcp/tools/prompts/flows.md → ai/tools/flows/prompt.md} +0 -0
  149. /package/dist/{mcp/tools/folders.d.ts → ai/tools/folders/index.d.ts} +0 -0
  150. /package/dist/{mcp/tools/prompts/folders.md → ai/tools/folders/prompt.md} +0 -0
  151. /package/dist/{mcp/tools/items.d.ts → ai/tools/items/index.d.ts} +0 -0
  152. /package/dist/{mcp/tools/prompts/operations.md → ai/tools/operations/prompt.md} +0 -0
  153. /package/dist/{mcp/tools/relations.d.ts → ai/tools/relations/index.d.ts} +0 -0
  154. /package/dist/{mcp/tools/prompts/relations.md → ai/tools/relations/prompt.md} +0 -0
  155. /package/dist/{mcp/tools/prompts/schema.md → ai/tools/schema/prompt.md} +0 -0
  156. /package/dist/{mcp → ai/tools}/schema.d.ts +0 -0
  157. /package/dist/{mcp → ai/tools}/schema.js +0 -0
  158. /package/dist/{mcp/tools/system.d.ts → ai/tools/system/index.d.ts} +0 -0
  159. /package/dist/{mcp/tools/prompts/system-prompt-description.md → ai/tools/system/prompt-description.md} +0 -0
  160. /package/dist/{mcp/tools/prompts/system-prompt.md → ai/tools/system/prompt.md} +0 -0
  161. /package/dist/{mcp/tools/trigger-flow.d.ts → ai/tools/trigger-flow/index.d.ts} +0 -0
  162. /package/dist/{mcp/tools/prompts/trigger-flow.md → ai/tools/trigger-flow/prompt.md} +0 -0
  163. /package/dist/{permissions/modules/fetch-global-access → ai/tools}/types.js +0 -0
  164. /package/dist/test-utils/{items-service.d.ts → services/items-service.d.ts} +0 -0
  165. /package/dist/test-utils/{items-service.js → services/items-service.js} +0 -0
@@ -63,10 +63,10 @@ export class CommentsService extends ItemsService {
63
63
  role: user['role']?.id ?? null,
64
64
  admin: false,
65
65
  app: false,
66
- roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
66
+ roles: await fetchRolesTree(user['role']?.id ?? null, { knex: this.knex }),
67
67
  ip: null,
68
68
  };
69
- const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
69
+ const userGlobalAccess = await fetchGlobalAccess(accountability, { knex: this.knex });
70
70
  accountability.admin = userGlobalAccess.admin;
71
71
  accountability.app = userGlobalAccess.app;
72
72
  const usersService = new UsersService({ schema: this.schema, accountability });
@@ -109,6 +109,8 @@ export class ExtensionsService {
109
109
  await this.extensionsManager.install(versionId);
110
110
  }
111
111
  async readAll() {
112
+ // wait for extensions to be reloaded
113
+ await this.extensionsManager.isReloading();
112
114
  const settings = await this.extensionsItemService.readByQuery({ limit: -1 });
113
115
  const regular = settings.filter(({ bundle }) => bundle === null);
114
116
  const bundled = settings.filter(({ bundle }) => bundle !== null);
@@ -138,6 +140,8 @@ export class ExtensionsService {
138
140
  return output;
139
141
  }
140
142
  async readOne(id) {
143
+ // wait for extensions to be reloaded
144
+ await this.extensionsManager.isReloading();
141
145
  const meta = await this.extensionsItemService.readOne(id);
142
146
  const schema = this.extensionsManager.getExtension(meta.source, meta.folder) ?? null;
143
147
  return {
@@ -1,5 +1,30 @@
1
- import type { AbstractServiceOptions } from '@directus/types';
1
+ import type { AbstractServiceOptions, Folder } from '@directus/types';
2
2
  import { ItemsService } from './items.js';
3
- export declare class FoldersService extends ItemsService {
3
+ export declare class FoldersService extends ItemsService<Folder> {
4
4
  constructor(options: AbstractServiceOptions);
5
+ /**
6
+ * Builds a full folder tree starting from a given root folder.
7
+ *
8
+ * This method returns a map of folder IDs to their corresponding paths
9
+ * relative to the root. It resolves all nested child folders and ensures
10
+ * that folder names are deduplicated within the same parent.
11
+ *
12
+ * Access control is applied automatically when non-admin, only folders the user has `read`
13
+ * access to are included.
14
+ *
15
+ * @param {string} root - The ID of the root folder to start building the tree from.
16
+ * @returns {Promise<Map<string, string>>} A `Map` where:
17
+ * - Key: folder ID
18
+ * - Value: folder path relative to the root (e.g., "Documents/Photos")
19
+ *
20
+ * @example
21
+ * const foldersService = new FoldersService({ schema, accountability });
22
+ * const tree = await foldersService.buildTree('root-folder-id');
23
+ * console.log(tree.get('folder1')); // e.g., "RootFolder/SubFolder1"
24
+ *
25
+ * @remarks
26
+ * - The returned `Map` includes the root folder itself.
27
+ * - If a folder has no name, its ID will be used as a fallback.
28
+ */
29
+ buildTree(root: string): Promise<Map<string, string>>;
5
30
  }
@@ -1,6 +1,81 @@
1
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
2
+ import { NameDeduper } from './assets/name-deduper.js';
1
3
  import { ItemsService } from './items.js';
2
4
  export class FoldersService extends ItemsService {
3
5
  constructor(options) {
4
6
  super('directus_folders', options);
5
7
  }
8
+ /**
9
+ * Builds a full folder tree starting from a given root folder.
10
+ *
11
+ * This method returns a map of folder IDs to their corresponding paths
12
+ * relative to the root. It resolves all nested child folders and ensures
13
+ * that folder names are deduplicated within the same parent.
14
+ *
15
+ * Access control is applied automatically when non-admin, only folders the user has `read`
16
+ * access to are included.
17
+ *
18
+ * @param {string} root - The ID of the root folder to start building the tree from.
19
+ * @returns {Promise<Map<string, string>>} A `Map` where:
20
+ * - Key: folder ID
21
+ * - Value: folder path relative to the root (e.g., "Documents/Photos")
22
+ *
23
+ * @example
24
+ * const foldersService = new FoldersService({ schema, accountability });
25
+ * const tree = await foldersService.buildTree('root-folder-id');
26
+ * console.log(tree.get('folder1')); // e.g., "RootFolder/SubFolder1"
27
+ *
28
+ * @remarks
29
+ * - The returned `Map` includes the root folder itself.
30
+ * - If a folder has no name, its ID will be used as a fallback.
31
+ */
32
+ async buildTree(root) {
33
+ if (this.accountability && this.accountability.admin !== true) {
34
+ await validateAccess({
35
+ collection: 'directus_folders',
36
+ accountability: this.accountability,
37
+ action: 'read',
38
+ primaryKeys: [root],
39
+ }, {
40
+ knex: this.knex,
41
+ schema: this.schema,
42
+ });
43
+ }
44
+ const folders = await this.readByQuery({ limit: -1 });
45
+ // build folder and child lookup
46
+ const folderLookup = new Map();
47
+ const childFolderLookup = new Map();
48
+ for (const folder of folders) {
49
+ if (!folder['id'])
50
+ continue;
51
+ folderLookup.set(folder['id'], folder);
52
+ // root is always at the top level, we can therfor safely skip any parent references to it.
53
+ if (folder['parent'] && folder['id'] !== root) {
54
+ const children = childFolderLookup.get(folder['parent']) ?? [];
55
+ children.push(folder['id']);
56
+ childFolderLookup.set(folder['parent'], children);
57
+ }
58
+ }
59
+ const deduper = new NameDeduper();
60
+ const rootName = deduper.add(folderLookup.get(root)?.name, { fallback: root });
61
+ const stack = [[root, '']];
62
+ const tree = new Map();
63
+ // build tree from stack
64
+ while (stack.length > 0) {
65
+ const [folderId, path] = stack.pop() ?? [];
66
+ if (!folderId)
67
+ continue;
68
+ const folder = folderLookup.get(folderId);
69
+ if (!folder)
70
+ continue;
71
+ const children = childFolderLookup.get(folderId);
72
+ const folderName = deduper.add(folder['name'], { group: folder['parent'], fallback: folderId });
73
+ const folderPath = path === '' ? rootName : `${path}/${folderName}`;
74
+ tree.set(folderId, folderPath);
75
+ for (const childFolderId of children ?? []) {
76
+ stack.push([childFolderId, folderPath]);
77
+ }
78
+ }
79
+ return tree;
80
+ }
6
81
  }
@@ -1,7 +1,7 @@
1
1
  import type { AbstractServiceOptions, Accountability, DirectusError, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { Readable } from 'node:stream';
4
- import type { FunctionFieldNode, FieldNode, NestedCollectionNode } from '../types/index.js';
4
+ import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../types/index.js';
5
5
  export declare function createErrorTracker(): {
6
6
  addCapturedError: (err: any, rowNumber: number) => void;
7
7
  buildFinalErrors: () => DirectusError<any>[];
@@ -1,22 +1,23 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { createError, ErrorCode, ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
3
3
  import { isSystemCollection } from '@directus/system-data';
4
- import { parseJSON, toArray } from '@directus/utils';
4
+ import { getDateTimeFormatted, parseJSON, toArray } from '@directus/utils';
5
5
  import { createTmpFile } from '@directus/utils/node';
6
6
  import { queue } from 'async';
7
7
  import destroyStream from 'destroy';
8
8
  import { dump as toYAML } from 'js-yaml';
9
9
  import { parse as toXML } from 'js2xmlparser';
10
10
  import { Parser as CSVParser, transforms as CSVTransforms } from 'json2csv';
11
+ import { set } from 'lodash-es';
11
12
  import { createReadStream, createWriteStream } from 'node:fs';
12
13
  import { appendFile } from 'node:fs/promises';
13
14
  import Papa from 'papaparse';
14
15
  import StreamArray from 'stream-json/streamers/StreamArray.js';
16
+ import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
15
17
  import getDatabase from '../database/index.js';
16
18
  import emitter from '../emitter.js';
17
19
  import { useLogger } from '../logger/index.js';
18
20
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
19
- import { getDateFormatted } from '../utils/get-date-formatted.js';
20
21
  import { getService } from '../utils/get-service.js';
21
22
  import { transaction } from '../utils/transaction.js';
22
23
  import { Url } from '../utils/url.js';
@@ -24,8 +25,6 @@ import { userName } from '../utils/user-name.js';
24
25
  import { FilesService } from './files.js';
25
26
  import { NotificationsService } from './notifications.js';
26
27
  import { UsersService } from './users.js';
27
- import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
28
- import { set } from 'lodash-es';
29
28
  const env = useEnv();
30
29
  const logger = useLogger();
31
30
  const MAX_IMPORT_ERRORS = env['MAX_IMPORT_ERRORS'];
@@ -514,7 +513,7 @@ export class ExportService {
514
513
  schema: this.schema,
515
514
  });
516
515
  const storage = toArray(env['STORAGE_LOCATIONS'])[0];
517
- const title = `export-${collection}-${getDateFormatted()}`;
516
+ const title = `export-${collection}-${getDateTimeFormatted()}`;
518
517
  const filename = `${title}.${format}`;
519
518
  const fileWithDefaults = {
520
519
  ...(options?.file ?? {}),
@@ -29,12 +29,12 @@ export class NotificationsService extends ItemsService {
29
29
  .addPath('admin', 'users', user['id'])
30
30
  .toString();
31
31
  const html = data.message ? md(data.message) : '';
32
- const roles = await fetchRolesTree(user['role'], this.knex);
32
+ const roles = await fetchRolesTree(user['role'], { knex: this.knex });
33
33
  const { app: app_access } = await fetchGlobalAccess({
34
34
  user: user['id'],
35
35
  roles,
36
36
  ip: null,
37
- }, this.knex);
37
+ }, { knex: this.knex });
38
38
  const mailService = new MailService({
39
39
  schema: this.schema,
40
40
  knex: this.knex,
@@ -10,6 +10,8 @@ import { parse as wktToGeoJSON } from 'wellknown';
10
10
  import { getHelpers } from '../database/helpers/index.js';
11
11
  import getDatabase from '../database/index.js';
12
12
  import { generateHash } from '../utils/generate-hash.js';
13
+ import { decrypt, encrypt } from '../utils/encrypt.js';
14
+ import { getSecret } from '../utils/get-secret.js';
13
15
  /**
14
16
  * Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
15
17
  * handled correctly.
@@ -124,6 +126,24 @@ export class PayloadService {
124
126
  }
125
127
  return value;
126
128
  },
129
+ async encrypt({ action, value, accountability }) {
130
+ if (!value)
131
+ return value;
132
+ if (action === 'read') {
133
+ // In-system calls can still get the decrypted value
134
+ if (accountability === null) {
135
+ const key = getSecret();
136
+ return await decrypt(value, key);
137
+ }
138
+ // Requests from the API entrypoints have accountability and shouldn't get the raw value
139
+ return '**********';
140
+ }
141
+ if (typeof value === 'string') {
142
+ const key = getSecret();
143
+ return await encrypt(value, key);
144
+ }
145
+ return value;
146
+ },
127
147
  };
128
148
  async processValues(action, payload, aliasMap = {}, aggregate = {}) {
129
149
  const processedPayload = toArray(payload);
@@ -3,8 +3,8 @@ import { UserIntegrityCheckFlag } from '@directus/types';
3
3
  import { clearSystemCache } from '../cache.js';
4
4
  import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
5
5
  import { transaction } from '../utils/transaction.js';
6
- import { ItemsService } from './items.js';
7
6
  import { AccessService } from './access.js';
7
+ import { ItemsService } from './items.js';
8
8
  import { PresetsService } from './presets.js';
9
9
  import { UsersService } from './users.js';
10
10
  export class RolesService extends ItemsService {
@@ -72,7 +72,7 @@ export class RolesService extends ItemsService {
72
72
  if (ids.includes(parent)) {
73
73
  throw new InvalidPayloadError({ reason: 'A role cannot be a parent of itself' });
74
74
  }
75
- const roles = await fetchRolesTree(parent, this.knex);
75
+ const roles = await fetchRolesTree(parent, { knex: this.knex });
76
76
  if (ids.some((id) => roles.includes(id))) {
77
77
  // The role tree up from the parent already includes this role, so it would create a circular reference
78
78
  throw new InvalidPayloadError({ reason: 'A role cannot have a parent that is already a descendant of itself' });
@@ -41,7 +41,7 @@ export async function createTusServer(context) {
41
41
  datastore: store,
42
42
  locker: getTusLocker(),
43
43
  ...(RESUMABLE_UPLOADS.MAX_SIZE !== null && { maxSize: RESUMABLE_UPLOADS.MAX_SIZE }),
44
- async onUploadFinish(req, upload) {
44
+ async onUploadFinish(_req, upload) {
45
45
  const schema = await getSchema();
46
46
  const service = new ItemsService('directus_files', {
47
47
  schema,
@@ -92,9 +92,9 @@ export async function createTusServer(context) {
92
92
  key: fileData.id,
93
93
  collection: 'directus_files',
94
94
  }, {
95
- database: getDatabase(),
96
95
  schema,
97
- accountability: req.accountability,
96
+ database: getDatabase(),
97
+ accountability: context.accountability ?? null,
98
98
  });
99
99
  return {
100
100
  headers: {
@@ -5,5 +5,20 @@ export type TelemetrySettings = {
5
5
  mcp_allow_deletes: boolean;
6
6
  mcp_system_prompt_enabled: boolean;
7
7
  visual_editor_urls: number;
8
+ ai_openai_api_key: boolean;
9
+ ai_anthropic_api_key: boolean;
10
+ ai_system_prompt: boolean;
11
+ };
12
+ export type DatabaseSettings = {
13
+ project_id: string;
14
+ mcp_enabled?: boolean;
15
+ mcp_allow_deletes?: boolean;
16
+ mcp_system_prompt_enabled?: boolean;
17
+ visual_editor_urls?: {
18
+ url: string;
19
+ }[];
20
+ ai_openai_api_key?: string;
21
+ ai_anthropic_api_key?: string;
22
+ ai_system_prompt?: string;
8
23
  };
9
24
  export declare const getSettings: (db: Knex) => Promise<TelemetrySettings>;
@@ -6,7 +6,16 @@ export const getSettings = async (db) => {
6
6
  schema: await getSchema({ database: db }),
7
7
  });
8
8
  const settings = (await settingsService.readSingleton({
9
- fields: ['project_id', 'mcp_enabled', 'mcp_allow_deletes', 'mcp_system_prompt_enabled', 'visual_editor_urls'],
9
+ fields: [
10
+ 'project_id',
11
+ 'mcp_enabled',
12
+ 'mcp_allow_deletes',
13
+ 'mcp_system_prompt_enabled',
14
+ 'visual_editor_urls',
15
+ 'ai_openai_api_key',
16
+ 'ai_anthropic_api_key',
17
+ 'ai_system_prompt',
18
+ ],
10
19
  }));
11
20
  return {
12
21
  project_id: settings.project_id,
@@ -14,5 +23,8 @@ export const getSettings = async (db) => {
14
23
  mcp_allow_deletes: settings?.mcp_allow_deletes || false,
15
24
  mcp_system_prompt_enabled: settings?.mcp_system_prompt_enabled || false,
16
25
  visual_editor_urls: settings.visual_editor_urls?.length || 0,
26
+ ai_openai_api_key: Boolean(settings?.ai_openai_api_key),
27
+ ai_anthropic_api_key: Boolean(settings?.ai_anthropic_api_key),
28
+ ai_system_prompt: Boolean(settings?.ai_system_prompt),
17
29
  };
18
30
  };
@@ -14,21 +14,23 @@ This directory contains mock implementations for commonly used modules in servic
14
14
  - **[emitter.ts](#emitterts)** - Event emitter mocks
15
15
  - **[items-service.ts](#items-servicets)** - ItemsService mocks
16
16
  - **[fields-service.ts](#fields-servicets)** - FieldsService mocks
17
+ - **[files-service.ts](#files-servicets)** - FilesService mocks
18
+ - **[folders-service.ts](#folders-servicets)** - FoldersService mocks
17
19
  - **[test-helpers.ts](#test-helpersts)** - Test data factory functions
18
20
 
19
21
  ## Quick Start
20
22
 
21
23
  ```typescript
22
- import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
24
+ import { createMockKnex, resetKnexMocks } from '../test-utils/knex.js';
23
25
 
24
26
  // Set up mocks
25
27
  vi.mock('../../src/database/index', async () => {
26
- const { mockDatabase } = await import('../__mocks__/database.js');
28
+ const { mockDatabase } = await import('../test-utils/database.js');
27
29
  return mockDatabase();
28
30
  });
29
31
 
30
32
  vi.mock('../cache.js', async () => {
31
- const { mockCache } = await import('../__mocks__/cache.js');
33
+ const { mockCache } = await import('../test-utils/cache.js');
32
34
  return mockCache();
33
35
  });
34
36
 
@@ -203,13 +205,13 @@ Creates a standard database module mock for service tests.
203
205
  ```typescript
204
206
  // Standard PostgreSQL mock
205
207
  vi.mock('../../src/database/index', async () => {
206
- const { mockDatabase } = await import('../__mocks__/database.js');
208
+ const { mockDatabase } = await import('../test-utils/database.js');
207
209
  return mockDatabase();
208
210
  });
209
211
 
210
212
  // MySQL-specific mock
211
213
  vi.mock('../../src/database/index', async () => {
212
- const { mockDatabase } = await import('../__mocks__/database.js');
214
+ const { mockDatabase } = await import('../test-utils/database.js');
213
215
  return mockDatabase('mysql');
214
216
  });
215
217
 
@@ -229,7 +231,7 @@ transaction wrapper).
229
231
 
230
232
  ```typescript
231
233
  vi.mock('../utils/transaction.js', async () => {
232
- const { mockTransaction } = await import('../__mocks__/database.js');
234
+ const { mockTransaction } = await import('../test-utils/database.js');
233
235
  return mockTransaction();
234
236
  });
235
237
 
@@ -269,13 +271,13 @@ mocks for vi.mock() declarations and spies for testing cache behavior.
269
271
  ```typescript
270
272
  // Standard usage for vi.mock()
271
273
  vi.mock('../cache.js', async () => {
272
- const { mockCache } = await import('../__mocks__/cache.js');
274
+ const { mockCache } = await import('../test-utils/cache.js');
273
275
  return mockCache();
274
276
  });
275
277
 
276
278
  // Testing cache clearing with spies
277
279
  import { getCache } from '../cache.js';
278
- import { mockCache } from '../__mocks__/cache.js';
280
+ import { mockCache } from '../test-utils/cache.js';
279
281
 
280
282
  test('should clear cache after update', async () => {
281
283
  const { spies } = mockCache();
@@ -305,7 +307,7 @@ Creates a standard schema inspector mock with tableInfo, columnInfo, primary, fo
305
307
  ```typescript
306
308
  // Standard usage
307
309
  vi.mock('@directus/schema', async () => {
308
- const { mockSchema } = await import('../__mocks__/schema.js');
310
+ const { mockSchema } = await import('../test-utils/schema.js');
309
311
  return mockSchema();
310
312
  });
311
313
 
@@ -340,7 +342,7 @@ Creates a standard emitter mock with emitAction, emitFilter, emitInit, and event
340
342
  ```typescript
341
343
  // Standard usage
342
344
  vi.mock('../emitter.js', async () => {
343
- const { mockEmitter } = await import('../__mocks__/emitter.js');
345
+ const { mockEmitter } = await import('../test-utils/emitter.js');
344
346
  return mockEmitter();
345
347
  });
346
348
 
@@ -390,7 +392,7 @@ Creates a standard ItemsService mock with all CRUD methods pre-configured with s
390
392
  ```typescript
391
393
  // Standard usage
392
394
  vi.mock('./items.js', async () => {
393
- const { mockItemsService } = await import('../__mocks__/items-service.js');
395
+ const { mockItemsService } = await import('../test-utils/services/items-service.js');
394
396
  return mockItemsService();
395
397
  });
396
398
 
@@ -423,6 +425,8 @@ Creates a standard FieldsService mock with common methods pre-configured.
423
425
 
424
426
  **Mocked methods:**
425
427
 
428
+ In addition to the base `ItemsService` method the following `FieldsService` specific methods are available:
429
+
426
430
  - `addColumnToTable` → no-op function
427
431
  - `addColumnIndex` → resolves to undefined
428
432
  - `deleteField` → resolves to undefined
@@ -434,7 +438,7 @@ Creates a standard FieldsService mock with common methods pre-configured.
434
438
  ```typescript
435
439
  // Standard usage in CollectionsService tests
436
440
  vi.mock('./fields.js', async () => {
437
- const { mockFieldsService } = await import('../__mocks__/fields-service.js');
441
+ const { mockFieldsService } = await import('../test-utils/services/fields-service.js');
438
442
  return mockFieldsService();
439
443
  });
440
444
 
@@ -450,43 +454,110 @@ expect(addColumnIndexSpy).toHaveBeenCalled();
450
454
 
451
455
  ---
452
456
 
457
+ ### files-service.ts
458
+
459
+ Provides FilesService mocking utilities for testing services that depend on FilesService.
460
+
461
+ #### `mockFilesService()`
462
+
463
+ Creates a standard FilesService mock with common methods pre-configured.
464
+
465
+ **Returns:** Mock module object with `FilesService` class
466
+
467
+ **Mocked methods:**
468
+
469
+ In addition to the base `ItemsService` method the following `FilesService` specific methods are available:
470
+
471
+ - `uploadOne` → `1`
472
+ - `importOne` → `1`
473
+
474
+ **Example:**
475
+
476
+ ```typescript
477
+ // Standard usage in service tests
478
+ vi.mock('./files.js', async () => {
479
+ const { mockFilesService } = await import('../test-utils/services/files-service.js');
480
+ return mockFilesService();
481
+ });
482
+
483
+ // Override specific methods during tests
484
+ import { FilesService } from './files.js';
485
+
486
+ const uploadOneSpy = vi.spyOn(FilesService.prototype, 'uploadOne').mockResolvedValue(`1`);
487
+ ```
488
+
489
+ ---
490
+
491
+ ### folders-service.ts
492
+
493
+ Provides FoldersService mocking utilities for testing services that depend on FoldersService.
494
+
495
+ #### `mockFilesService()`
496
+
497
+ Creates a standard FoldersService mock with common methods pre-configured.
498
+
499
+ **Returns:** Mock module object with `FoldersService` class
500
+
501
+ **Mocked methods:**
502
+
503
+ In addition to the base `ItemsService` method the following `FoldersService` specific methods are available:
504
+
505
+ - `buildTree` → return `1` => `root` map
506
+
507
+ **Example:**
508
+
509
+ ```typescript
510
+ // Standard usage in service tests
511
+ vi.mock('./folders.js', async () => {
512
+ const { mockFoldersService } = await import('../test-utils/services/folders-service.js');
513
+ return mockFilesService();
514
+ });
515
+
516
+ // Override specific methods during tests
517
+ import { FoldersService } from './folders.js';
518
+
519
+ const buildTreeSpy = vi.spyOn(FoldersService.prototype, 'buildTree').mockResolvedValue(new Map('1', 'root-alt'));
520
+ ```
521
+
522
+ ---
523
+
453
524
  ## Common Patterns
454
525
 
455
526
  ### Full Service Test Setup
456
527
 
457
528
  ```typescript
458
- import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
529
+ import { createMockKnex, resetKnexMocks } from '../test-utils/knex.js';
459
530
  import { SchemaBuilder } from '@directus/schema-builder';
460
531
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
461
532
 
462
533
  // Mock all dependencies (before imports)
463
534
  vi.mock('../../src/database/index', async () => {
464
- const { mockDatabase } = await import('../__mocks__/database.js');
535
+ const { mockDatabase } = await import('../test-utils/database.js');
465
536
  return mockDatabase();
466
537
  });
467
538
 
468
539
  vi.mock('@directus/schema', async () => {
469
- const { mockSchema } = await import('../__mocks__/schema.js');
540
+ const { mockSchema } = await import('../test-utils/schema.js');
470
541
  return mockSchema();
471
542
  });
472
543
 
473
544
  vi.mock('../cache.js', async () => {
474
- const { mockCache } = await import('../__mocks__/cache.js');
545
+ const { mockCache } = await import('../test-utils/cache.js');
475
546
  return mockCache();
476
547
  });
477
548
 
478
549
  vi.mock('../emitter.js', async () => {
479
- const { mockEmitter } = await import('../__mocks__/emitter.js');
550
+ const { mockEmitter } = await import('../test-utils/emitter.js');
480
551
  return mockEmitter();
481
552
  });
482
553
 
483
554
  vi.mock('./items.js', async () => {
484
- const { mockItemsService } = await import('../__mocks__/items-service.js');
555
+ const { mockItemsService } = await import('../test-utils/services/items-service.js');
485
556
  return mockItemsService();
486
557
  });
487
558
 
488
559
  vi.mock('../utils/transaction.js', async () => {
489
- const { mockTransaction } = await import('../__mocks__/database.js');
560
+ const { mockTransaction } = await import('../test-utils/database.js');
490
561
  return mockTransaction();
491
562
  });
492
563
 
@@ -525,7 +596,7 @@ describe('Integration Tests', () => {
525
596
  ### Testing Schema Operations
526
597
 
527
598
  ```typescript
528
- import { mockCreateTable, mockAlterTable, createMockTableBuilder } from '../__mocks__/knex.js';
599
+ import { mockCreateTable, mockAlterTable, createMockTableBuilder } from '../test-utils/knex.js';
529
600
 
530
601
  test('should create table with correct schema', async () => {
531
602
  const { db, mockSchemaBuilder } = createMockKnex();
@@ -552,7 +623,7 @@ test('should alter table to add column', async () => {
552
623
 
553
624
  ```typescript
554
625
  import { getCache } from '../cache.js';
555
- import { mockCache } from '../__mocks__/cache.js';
626
+ import { mockCache } from '../test-utils/cache.js';
556
627
 
557
628
  test('should clear cache after update', async () => {
558
629
  const { spies } = mockCache();
@@ -604,7 +675,7 @@ test('should read column info', async () => {
604
675
  ### Testing with System Collection Mocks
605
676
 
606
677
  ```typescript
607
- import { setupSystemCollectionMocks } from '../__mocks__/knex.js';
678
+ import { setupSystemCollectionMocks } from '../test-utils/knex.js';
608
679
 
609
680
  describe('Service Tests', () => {
610
681
  const { db, tracker, mockSchemaBuilder } = createMockKnex();
@@ -678,7 +749,7 @@ Always declare `vi.mock()` calls **before** importing the modules they mock:
678
749
  ```typescript
679
750
  // ✅ Correct - mocks first
680
751
  vi.mock('../cache.js', async () => {
681
- const { mockCache } = await import('../__mocks__/cache.js');
752
+ const { mockCache } = await import('../test-utils/cache.js');
682
753
  return mockCache();
683
754
  });
684
755
 
@@ -688,7 +759,7 @@ import { YourService } from './your-service.js';
688
759
  import { YourService } from './your-service.js';
689
760
 
690
761
  vi.mock('../cache.js', async () => {
691
- const { mockCache } = await import('../__mocks__/cache.js');
762
+ const { mockCache } = await import('../test-utils/cache.js');
692
763
  return mockCache();
693
764
  });
694
765
  ```
@@ -12,13 +12,13 @@
12
12
  * ```typescript
13
13
  * // Standard usage for vi.mock()
14
14
  * vi.mock('../cache.js', async () => {
15
- * const { mockCache } = await import('../__mocks__/cache.js');
15
+ * const { mockCache } = await import('../test-utils/cache.js');
16
16
  * return mockCache();
17
17
  * });
18
18
  *
19
19
  * // Testing cache clearing with spies
20
20
  * import { getCache } from '../cache.js';
21
- * import { mockCache } from '../__mocks__/cache.js';
21
+ * import { mockCache } from '../test-utils/cache.js';
22
22
  *
23
23
  * test('should clear cache after update', async () => {
24
24
  * const { spies } = mockCache();
@@ -13,13 +13,13 @@ import { vi } from 'vitest';
13
13
  * ```typescript
14
14
  * // Standard usage for vi.mock()
15
15
  * vi.mock('../cache.js', async () => {
16
- * const { mockCache } = await import('../__mocks__/cache.js');
16
+ * const { mockCache } = await import('../test-utils/cache.js');
17
17
  * return mockCache();
18
18
  * });
19
19
  *
20
20
  * // Testing cache clearing with spies
21
21
  * import { getCache } from '../cache.js';
22
- * import { mockCache } from '../__mocks__/cache.js';
22
+ * import { mockCache } from '../test-utils/cache.js';
23
23
  *
24
24
  * test('should clear cache after update', async () => {
25
25
  * const { spies } = mockCache();
@@ -12,7 +12,7 @@
12
12
  * ```typescript
13
13
  * // Standard usage
14
14
  * vi.mock('./fields.js', async () => {
15
- * const { mockFieldsService } = await import('../__mocks__/fields-service.js');
15
+ * const { mockFieldsService } = await import('../test-utils/services/fields-service.js');
16
16
  * return mockFieldsService();
17
17
  * });
18
18
  *