@directus/api 14.1.2 → 16.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 (207) hide show
  1. package/dist/app.js +8 -6
  2. package/dist/auth/drivers/ldap.js +7 -4
  3. package/dist/auth/drivers/local.js +3 -2
  4. package/dist/auth/drivers/oauth2.js +11 -5
  5. package/dist/auth/drivers/openid.js +11 -5
  6. package/dist/auth/drivers/saml.js +6 -4
  7. package/dist/auth.js +7 -4
  8. package/dist/bus/index.d.ts +1 -0
  9. package/dist/bus/index.js +1 -0
  10. package/dist/bus/lib/use-bus.d.ts +9 -0
  11. package/dist/bus/lib/use-bus.js +21 -0
  12. package/dist/cache.js +9 -9
  13. package/dist/cli/commands/bootstrap/index.js +6 -2
  14. package/dist/cli/commands/count/index.js +2 -1
  15. package/dist/cli/commands/database/install.js +2 -1
  16. package/dist/cli/commands/database/migrate.js +2 -1
  17. package/dist/cli/commands/roles/create.js +2 -1
  18. package/dist/cli/commands/schema/apply.js +46 -34
  19. package/dist/cli/commands/schema/snapshot.js +6 -5
  20. package/dist/cli/commands/users/create.js +4 -3
  21. package/dist/cli/commands/users/passwd.js +5 -4
  22. package/dist/cli/index.js +2 -2
  23. package/dist/cli/load-extensions.js +4 -2
  24. package/dist/cli/utils/create-env/env-stub.liquid +1 -1
  25. package/dist/constants.d.ts +1 -1
  26. package/dist/constants.js +4 -1
  27. package/dist/controllers/assets.js +5 -3
  28. package/dist/controllers/auth.js +5 -4
  29. package/dist/controllers/extensions.js +18 -6
  30. package/dist/controllers/files.js +3 -3
  31. package/dist/controllers/schema.js +3 -2
  32. package/dist/controllers/shares.js +3 -3
  33. package/dist/database/helpers/index.d.ts +1 -1
  34. package/dist/database/index.d.ts +2 -1
  35. package/dist/database/index.js +11 -3
  36. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +3 -1
  37. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -1
  38. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  39. package/dist/database/migrations/20230721A-require-shares-fields.js +2 -1
  40. package/dist/database/migrations/20231215A-add-focalpoints.d.ts +3 -0
  41. package/dist/database/migrations/20231215A-add-focalpoints.js +12 -0
  42. package/dist/database/migrations/run.js +2 -1
  43. package/dist/database/run-ast.js +5 -2
  44. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -7
  45. package/dist/database/system-data/fields/files.yaml +16 -0
  46. package/dist/database/system-data/relations/relations.yaml +4 -0
  47. package/dist/emitter.d.ts +1 -0
  48. package/dist/emitter.js +4 -1
  49. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  50. package/dist/extensions/lib/get-extensions-path.js +2 -1
  51. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  52. package/dist/extensions/lib/get-extensions.js +32 -8
  53. package/dist/extensions/lib/get-shared-deps-mapping.js +7 -5
  54. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  55. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -0
  56. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  57. package/dist/extensions/lib/sync-extensions.js +6 -4
  58. package/dist/extensions/manager.d.ts +5 -0
  59. package/dist/extensions/manager.js +84 -34
  60. package/dist/flows.js +13 -7
  61. package/dist/logger.d.ts +7 -6
  62. package/dist/logger.js +116 -91
  63. package/dist/mailer.js +4 -2
  64. package/dist/middleware/cache.js +4 -2
  65. package/dist/middleware/check-ip.js +25 -6
  66. package/dist/middleware/cors.js +2 -1
  67. package/dist/middleware/error-handler.js +5 -5
  68. package/dist/middleware/rate-limiter-global.js +4 -2
  69. package/dist/middleware/rate-limiter-ip.js +16 -12
  70. package/dist/middleware/respond.js +4 -2
  71. package/dist/operations/log/index.js +2 -1
  72. package/dist/rate-limiter.d.ts +2 -1
  73. package/dist/rate-limiter.js +5 -2
  74. package/dist/redis/index.d.ts +3 -0
  75. package/dist/redis/index.js +3 -0
  76. package/dist/redis/lib/create-redis.d.ts +7 -0
  77. package/dist/redis/lib/create-redis.js +12 -0
  78. package/dist/redis/lib/use-redis.d.ts +16 -0
  79. package/dist/redis/lib/use-redis.js +22 -0
  80. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  81. package/dist/redis/utils/redis-config-available.js +8 -0
  82. package/dist/request/request-interceptor.js +7 -5
  83. package/dist/request/response-interceptor.js +2 -2
  84. package/dist/request/validate-ip.d.ts +1 -1
  85. package/dist/request/validate-ip.js +23 -7
  86. package/dist/server.d.ts +2 -0
  87. package/dist/server.js +11 -7
  88. package/dist/services/activity.js +5 -4
  89. package/dist/services/assets.d.ts +2 -0
  90. package/dist/services/assets.js +9 -4
  91. package/dist/services/authentication.js +17 -9
  92. package/dist/services/collections.js +5 -4
  93. package/dist/services/extensions.d.ts +15 -9
  94. package/dist/services/extensions.js +75 -40
  95. package/dist/services/fields.js +9 -4
  96. package/dist/services/files.d.ts +2 -2
  97. package/dist/services/files.js +22 -14
  98. package/dist/services/graphql/index.js +96 -18
  99. package/dist/services/graphql/subscription.js +2 -2
  100. package/dist/services/graphql/types/bigint.js +16 -5
  101. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  102. package/dist/services/graphql/utils/process-error.js +10 -8
  103. package/dist/services/import-export/index.js +5 -3
  104. package/dist/services/items.js +12 -8
  105. package/dist/services/mail/index.js +4 -2
  106. package/dist/services/notifications.js +7 -3
  107. package/dist/services/payload.js +3 -3
  108. package/dist/services/relations.js +19 -10
  109. package/dist/services/server.js +7 -7
  110. package/dist/services/shares.js +3 -2
  111. package/dist/services/specifications.js +5 -4
  112. package/dist/services/users.js +24 -13
  113. package/dist/services/versions.js +6 -5
  114. package/dist/services/webhooks.d.ts +2 -2
  115. package/dist/services/webhooks.js +2 -2
  116. package/dist/services/websocket.d.ts +1 -1
  117. package/dist/services/websocket.js +4 -3
  118. package/dist/storage/register-drivers.js +2 -1
  119. package/dist/storage/register-locations.js +2 -1
  120. package/dist/synchronization.js +3 -1
  121. package/dist/telemetry/index.d.ts +4 -0
  122. package/dist/telemetry/index.js +4 -0
  123. package/dist/telemetry/lib/get-report.d.ts +5 -0
  124. package/dist/telemetry/lib/get-report.js +42 -0
  125. package/dist/telemetry/lib/init-telemetry.d.ts +11 -0
  126. package/dist/telemetry/lib/init-telemetry.js +30 -0
  127. package/dist/telemetry/lib/send-report.d.ts +5 -0
  128. package/dist/telemetry/lib/send-report.js +23 -0
  129. package/dist/telemetry/lib/track.d.ts +10 -0
  130. package/dist/telemetry/lib/track.js +30 -0
  131. package/dist/telemetry/types/report.d.ts +58 -0
  132. package/dist/telemetry/types/report.js +1 -0
  133. package/dist/telemetry/utils/get-item-count.d.ts +26 -0
  134. package/dist/telemetry/utils/get-item-count.js +36 -0
  135. package/dist/telemetry/utils/get-random-wait-time.d.ts +5 -0
  136. package/dist/telemetry/utils/get-random-wait-time.js +5 -0
  137. package/dist/telemetry/utils/get-user-count.d.ts +7 -0
  138. package/dist/telemetry/utils/get-user-count.js +30 -0
  139. package/dist/telemetry/utils/get-user-item-count.d.ts +13 -0
  140. package/dist/telemetry/utils/get-user-item-count.js +18 -0
  141. package/dist/types/assets.d.ts +2 -0
  142. package/dist/utils/apply-diff.js +2 -1
  143. package/dist/utils/apply-query.js +2 -2
  144. package/dist/utils/delete-from-require-cache.js +2 -1
  145. package/dist/utils/get-accountability-for-token.js +3 -2
  146. package/dist/utils/get-auth-providers.js +2 -1
  147. package/dist/utils/get-cache-headers.js +5 -2
  148. package/dist/utils/get-cache-key.js +1 -1
  149. package/dist/utils/get-config-from-env.js +2 -1
  150. package/dist/utils/get-default-value.js +4 -3
  151. package/dist/utils/get-ip-from-req.d.ts +1 -1
  152. package/dist/utils/get-ip-from-req.js +5 -3
  153. package/dist/utils/get-permissions.js +5 -3
  154. package/dist/utils/get-schema.js +5 -2
  155. package/dist/utils/get-snapshot-diff.js +7 -9
  156. package/dist/utils/get-snapshot.js +5 -5
  157. package/dist/utils/get-versioned-hash.js +1 -1
  158. package/dist/utils/ip-in-networks.d.ts +6 -0
  159. package/dist/utils/ip-in-networks.js +13 -0
  160. package/dist/utils/is-url-allowed.js +2 -1
  161. package/dist/utils/job-queue.d.ts +1 -0
  162. package/dist/utils/job-queue.js +3 -0
  163. package/dist/utils/md.d.ts +1 -1
  164. package/dist/utils/md.js +3 -2
  165. package/dist/utils/sanitize-query.js +7 -2
  166. package/dist/utils/sanitize-schema.d.ts +1 -1
  167. package/dist/utils/should-clear-cache.js +2 -1
  168. package/dist/utils/should-skip-cache.js +2 -1
  169. package/dist/utils/transformations.js +95 -12
  170. package/dist/utils/validate-env.js +4 -2
  171. package/dist/utils/validate-query.js +8 -3
  172. package/dist/utils/validate-snapshot.js +3 -3
  173. package/dist/utils/validate-storage.js +4 -2
  174. package/dist/webhooks.js +4 -3
  175. package/dist/websocket/controllers/base.d.ts +2 -0
  176. package/dist/websocket/controllers/base.js +12 -6
  177. package/dist/websocket/controllers/graphql.d.ts +2 -0
  178. package/dist/websocket/controllers/graphql.js +5 -3
  179. package/dist/websocket/controllers/hooks.js +3 -2
  180. package/dist/websocket/controllers/index.d.ts +2 -0
  181. package/dist/websocket/controllers/index.js +4 -2
  182. package/dist/websocket/controllers/rest.d.ts +2 -0
  183. package/dist/websocket/controllers/rest.js +4 -2
  184. package/dist/websocket/errors.js +2 -1
  185. package/dist/websocket/handlers/heartbeat.js +4 -3
  186. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  187. package/dist/websocket/handlers/subscribe.js +5 -4
  188. package/dist/websocket/types.d.ts +3 -1
  189. package/package.json +114 -115
  190. package/dist/__utils__/items-utils.d.ts +0 -2
  191. package/dist/__utils__/items-utils.js +0 -31
  192. package/dist/__utils__/mock-env.d.ts +0 -18
  193. package/dist/__utils__/mock-env.js +0 -41
  194. package/dist/__utils__/schemas.d.ts +0 -13
  195. package/dist/__utils__/schemas.js +0 -301
  196. package/dist/__utils__/snapshots.d.ts +0 -5
  197. package/dist/__utils__/snapshots.js +0 -903
  198. package/dist/env.d.ts +0 -13
  199. package/dist/env.js +0 -505
  200. package/dist/messenger.d.ts +0 -24
  201. package/dist/messenger.js +0 -64
  202. package/dist/utils/package.d.ts +0 -2
  203. package/dist/utils/package.js +0 -6
  204. package/dist/utils/telemetry.d.ts +0 -1
  205. package/dist/utils/telemetry.js +0 -23
  206. package/dist/utils/to-boolean.d.ts +0 -4
  207. package/dist/utils/to-boolean.js +0 -6
@@ -1,13 +1,13 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { toArray } from '@directus/utils';
2
3
  import { clone, cloneDeep, isNil, merge, pick, uniq } from 'lodash-es';
3
- import { getHelpers } from './helpers/index.js';
4
- import env from '../env.js';
5
4
  import { PayloadService } from '../services/payload.js';
6
5
  import { applyFunctionToColumnName } from '../utils/apply-function-to-column-name.js';
7
6
  import applyQuery, { applyLimit, applySort, generateAlias } from '../utils/apply-query.js';
8
7
  import { getCollectionFromAlias } from '../utils/get-collection-from-alias.js';
9
8
  import { getColumn } from '../utils/get-column.js';
10
9
  import { stripFunction } from '../utils/strip-function.js';
10
+ import { getHelpers } from './helpers/index.js';
11
11
  import getDatabase from './index.js';
12
12
  /**
13
13
  * Execute a given AST using Knex. Returns array of items based on requested AST.
@@ -26,6 +26,7 @@ export default async function runAST(originalAST, schema, options) {
26
26
  return await run(ast.name, ast.children, options?.query || ast.query);
27
27
  }
28
28
  async function run(collection, children, query) {
29
+ const env = useEnv();
29
30
  // Retrieve the database columns to select in the current AST
30
31
  const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(schema, collection, children, query);
31
32
  // The actual knex query builder instance. This is a promise that resolves with the raw items from the db
@@ -148,6 +149,7 @@ function getColumnPreprocessor(knex, schema, table) {
148
149
  };
149
150
  }
150
151
  async function getDBQuery(schema, knex, table, fieldNodes, query) {
152
+ const env = useEnv();
151
153
  const preProcess = getColumnPreprocessor(knex, schema, table);
152
154
  const queryCopy = clone(query);
153
155
  const helpers = getHelpers(knex);
@@ -296,6 +298,7 @@ function applyParentFilters(schema, nestedCollectionNodes, parentItem) {
296
298
  return nestedCollectionNodes;
297
299
  }
298
300
  function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode) {
301
+ const env = useEnv();
299
302
  const nestedItems = toArray(nestedItem);
300
303
  const parentItems = clone(toArray(parentItem));
301
304
  if (nestedNode.type === 'm2o') {
@@ -102,13 +102,6 @@
102
102
  - appearance
103
103
  - theme_light
104
104
  - theme_dark
105
- - theme_light_overrides
106
- - theme_dark_overrides
107
105
  - tfa_secret
108
106
  - status
109
107
  - role
110
-
111
- # This is a temporary allowed field to help people migrate from
112
- # 10.6 to 10.7 and should be removed in 10.8
113
- # @TODO remove
114
- - theme
@@ -46,6 +46,22 @@ fields:
46
46
  width: half
47
47
  readonly: true
48
48
 
49
+ - field: focal_point_divider
50
+ interface: presentation-divider
51
+ options:
52
+ icon: image_search
53
+ title: $t:field_options.directus_files.focal_point_divider
54
+ special:
55
+ - alias
56
+ - no-data
57
+ width: full
58
+
59
+ - field: focal_point_x
60
+ width: half
61
+
62
+ - field: focal_point_y
63
+ width: half
64
+
49
65
  - field: storage_divider
50
66
  interface: presentation-divider
51
67
  options:
@@ -96,6 +96,10 @@ data:
96
96
  many_field: public_background
97
97
  one_collection: directus_files
98
98
 
99
+ - many_collection: directus_settings
100
+ many_field: public_favicon
101
+ one_collection: directus_files
102
+
99
103
  - many_collection: directus_settings
100
104
  many_field: storage_default_folder
101
105
  one_collection: directus_folders
package/dist/emitter.d.ts CHANGED
@@ -17,4 +17,5 @@ export declare class Emitter {
17
17
  offAll(): void;
18
18
  }
19
19
  declare const emitter: Emitter;
20
+ export declare const useEmitter: () => Emitter;
20
21
  export default emitter;
package/dist/emitter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ee2 from 'eventemitter2';
2
- import logger from './logger.js';
3
2
  import getDatabase from './database/index.js';
3
+ import { useLogger } from './logger.js';
4
4
  export class Emitter {
5
5
  filterEmitter;
6
6
  actionEmitter;
@@ -42,6 +42,7 @@ export class Emitter {
42
42
  return updatedPayload;
43
43
  }
44
44
  emitAction(event, meta, context = null) {
45
+ const logger = useLogger();
45
46
  const events = Array.isArray(event) ? event : [event];
46
47
  for (const event of events) {
47
48
  this.actionEmitter.emitAsync(event, { event, ...meta }, context ?? this.getDefaultContext()).catch((err) => {
@@ -51,6 +52,7 @@ export class Emitter {
51
52
  }
52
53
  }
53
54
  async emitInit(event, meta) {
55
+ const logger = useLogger();
54
56
  try {
55
57
  await this.initEmitter.emitAsync(event, { event, ...meta });
56
58
  }
@@ -84,4 +86,5 @@ export class Emitter {
84
86
  }
85
87
  }
86
88
  const emitter = new Emitter();
89
+ export const useEmitter = () => emitter;
87
90
  export default emitter;
@@ -1 +1 @@
1
- export declare const getExtensionsPath: () => any;
1
+ export declare const getExtensionsPath: () => string;
@@ -1,6 +1,7 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { join } from 'path';
2
- import env from '../../env.js';
3
3
  export const getExtensionsPath = () => {
4
+ const env = useEnv();
4
5
  if (env['EXTENSIONS_LOCATION']) {
5
6
  return join(env['TEMP_PATH'], 'extensions');
6
7
  }
@@ -1 +1 @@
1
- export declare const getExtensions: () => Promise<import("@directus/extensions/node").Extension[]>;
1
+ export declare const getExtensions: () => Promise<any[]>;
@@ -1,12 +1,36 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { getLocalExtensions, getPackageExtensions, resolvePackageExtensions } from '@directus/extensions/node';
2
- import env from '../../env.js';
3
+ import { useLogger } from '../../logger.js';
3
4
  import { getExtensionsPath } from './get-extensions-path.js';
4
5
  export const getExtensions = async () => {
5
- const localExtensions = await getLocalExtensions(getExtensionsPath());
6
- const loadedNames = localExtensions.map(({ name }) => name);
7
- const filterDuplicates = ({ name }) => loadedNames.includes(name) === false;
8
- const localPackageExtensions = (await resolvePackageExtensions(getExtensionsPath())).filter((extension) => filterDuplicates(extension));
9
- loadedNames.push(...localPackageExtensions.map(({ name }) => name));
10
- const packageExtensions = (await getPackageExtensions(env['PACKAGE_FILE_LOCATION'])).filter((extension) => filterDuplicates(extension));
11
- return [...packageExtensions, ...localPackageExtensions, ...localExtensions];
6
+ const env = useEnv();
7
+ const logger = useLogger();
8
+ const loadedExtensions = new Map();
9
+ const duplicateExtensions = [];
10
+ const filterDuplicates = (extension) => {
11
+ const isExistingExtension = loadedExtensions.has(extension.name);
12
+ if (isExistingExtension) {
13
+ duplicateExtensions.push(extension.name);
14
+ return;
15
+ }
16
+ if (extension.type === 'bundle') {
17
+ const bundleEntryNames = new Set();
18
+ for (const entry of extension.entries) {
19
+ if (bundleEntryNames.has(entry.name)) {
20
+ // Do not load entire bundle if it has duplicated entries
21
+ duplicateExtensions.push(extension.name);
22
+ return;
23
+ }
24
+ bundleEntryNames.add(entry.name);
25
+ }
26
+ }
27
+ loadedExtensions.set(extension.name, extension);
28
+ };
29
+ (await getLocalExtensions(getExtensionsPath())).forEach(filterDuplicates);
30
+ (await resolvePackageExtensions(getExtensionsPath())).forEach(filterDuplicates);
31
+ (await getPackageExtensions(env['PACKAGE_FILE_LOCATION'])).forEach(filterDuplicates);
32
+ if (duplicateExtensions.length > 0) {
33
+ logger.warn(`Failed to load the following extensions because they have/contain duplicate names: ${duplicateExtensions.join(', ')}`);
34
+ }
35
+ return Array.from(loadedExtensions.values());
12
36
  };
@@ -1,18 +1,20 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { resolvePackage } from '@directus/utils/node';
2
3
  import { escapeRegExp } from 'lodash-es';
3
4
  import { readdir } from 'node:fs/promises';
4
- import path from 'path';
5
- import env from '../../env.js';
6
- import logger from '../../logger.js';
7
- import { Url } from '../../utils/url.js';
8
5
  import { dirname } from 'node:path';
9
6
  import { fileURLToPath } from 'node:url';
7
+ import path from 'path';
8
+ import { useLogger } from '../../logger.js';
9
+ import { Url } from '../../utils/url.js';
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  export const getSharedDepsMapping = async (deps) => {
12
+ const env = useEnv();
13
+ const logger = useLogger();
12
14
  const appDir = await readdir(path.join(resolvePackage('@directus/app', __dirname), 'dist', 'assets'));
13
15
  const depsMapping = {};
14
16
  for (const dep of deps) {
15
- const depRegex = new RegExp(`${escapeRegExp(dep.replace(/\//g, '_'))}\\.[0-9a-f]{8}\\.entry\\.js`);
17
+ const depRegex = new RegExp(`${escapeRegExp(dep.replace(/\//g, '_'))}\\.[a-zA-Z0-9_-]{8}\\.entry\\.js`);
16
18
  const depName = appDir.find((file) => depRegex.test(file));
17
19
  if (depName) {
18
20
  const depUrl = new Url(env['PUBLIC_URL']).addPath('admin', 'assets', depName);
@@ -1,6 +1,8 @@
1
- import env from '../../../../env.js';
2
- import logger from '../../../../logger.js';
1
+ import { useEnv } from '@directus/env';
2
+ import { useLogger } from '../../../../logger.js';
3
3
  export async function callReference(fn, args) {
4
+ const env = useEnv();
5
+ const logger = useLogger();
4
6
  const sandboxTimeout = Number(env['EXTENSIONS_SANDBOX_TIMEOUT']);
5
7
  try {
6
8
  return await fn.apply(undefined, args, {
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  import type { Router } from 'express';
2
3
  import type { Reference } from 'isolated-vm';
3
4
  import type { IncomingHttpHeaders } from 'node:http';
@@ -1,5 +1,6 @@
1
- import logger from '../../../../../logger.js';
1
+ import { useLogger } from '../../../../../logger.js';
2
2
  export function logGenerator(requestedScopes) {
3
+ const logger = useLogger();
3
4
  return (message) => {
4
5
  if (requestedScopes.log === undefined)
5
6
  throw new Error('No permission to access "log"');
@@ -1,3 +1,4 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { NESTED_EXTENSION_TYPES } from '@directus/extensions';
2
3
  import { ensureExtensionDirs } from '@directus/extensions/node';
3
4
  import mid from 'node-machine-id';
@@ -6,19 +7,20 @@ import { mkdir } from 'node:fs/promises';
6
7
  import { dirname, join, relative, resolve, sep } from 'node:path';
7
8
  import { pipeline } from 'node:stream/promises';
8
9
  import Queue from 'p-queue';
9
- import env from '../../env.js';
10
- import logger from '../../logger.js';
11
- import { getMessenger } from '../../messenger.js';
10
+ import { useBus } from '../../bus/index.js';
11
+ import { useLogger } from '../../logger.js';
12
12
  import { getStorage } from '../../storage/index.js';
13
13
  import { getExtensionsPath } from './get-extensions-path.js';
14
14
  import { SyncStatus, getSyncStatus, setSyncStatus } from './sync-status.js';
15
15
  export const syncExtensions = async () => {
16
+ const env = useEnv();
17
+ const logger = useLogger();
16
18
  const extensionsPath = getExtensionsPath();
17
19
  if (!env['EXTENSIONS_LOCATION']) {
18
20
  // Safe to run with multiple instances since dirs are created with `recursive: true`
19
21
  return ensureExtensionDirs(extensionsPath, NESTED_EXTENSION_TYPES);
20
22
  }
21
- const messenger = getMessenger();
23
+ const messenger = useBus();
22
24
  const isPrimaryProcess = String(process.env['NODE_APP_INSTANCE']) === '0' || process.env['NODE_APP_INSTANCE'] === undefined;
23
25
  const id = await mid.machineId();
24
26
  const message = `extensions-sync/${id}`;
@@ -155,4 +155,9 @@ export declare class ExtensionManager {
155
155
  * Remove the registration for all API extensions
156
156
  */
157
157
  private unregisterApiExtensions;
158
+ /**
159
+ * If extensions must load successfully, any errors will cause the process to exit.
160
+ * Otherwise, the error will only be logged as a warning.
161
+ */
162
+ private handleExtensionError;
158
163
  }
@@ -1,25 +1,26 @@
1
1
  import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
2
3
  import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES, NESTED_EXTENSION_TYPES } from '@directus/extensions';
3
4
  import { generateExtensionsEntrypoint } from '@directus/extensions/node';
4
- import { isIn, isTypeIn, pluralize } from '@directus/utils';
5
- import { pathToRelativeUrl } from '@directus/utils/node';
5
+ import { isIn, isTypeIn, pluralize, toBoolean } from '@directus/utils';
6
+ import { getNodeEnv } from '@directus/utils/node';
6
7
  import aliasDefault from '@rollup/plugin-alias';
7
8
  import nodeResolveDefault from '@rollup/plugin-node-resolve';
8
9
  import virtualDefault from '@rollup/plugin-virtual';
9
10
  import chokidar, { FSWatcher } from 'chokidar';
10
11
  import express, { Router } from 'express';
11
12
  import ivm from 'isolated-vm';
12
- import { clone } from 'lodash-es';
13
+ import { clone, debounce } from 'lodash-es';
13
14
  import { readFile, readdir } from 'node:fs/promises';
15
+ import os from 'node:os';
14
16
  import { dirname } from 'node:path';
15
17
  import { fileURLToPath } from 'node:url';
16
18
  import path from 'path';
17
19
  import { rollup } from 'rollup';
18
20
  import getDatabase from '../database/index.js';
19
21
  import emitter, { Emitter } from '../emitter.js';
20
- import env from '../env.js';
21
22
  import { getFlowManager } from '../flows.js';
22
- import logger from '../logger.js';
23
+ import { useLogger } from '../logger.js';
23
24
  import * as services from '../services/index.js';
24
25
  import { deleteFromRequireCache } from '../utils/delete-from-require-cache.js';
25
26
  import getModuleDefault from '../utils/get-module-default.js';
@@ -40,9 +41,10 @@ const virtual = virtualDefault;
40
41
  const alias = aliasDefault;
41
42
  const nodeResolve = nodeResolveDefault;
42
43
  const __dirname = dirname(fileURLToPath(import.meta.url));
44
+ const env = useEnv();
43
45
  const defaultOptions = {
44
46
  schedule: true,
45
- watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
47
+ watch: env['EXTENSIONS_AUTO_RELOAD'] && getNodeEnv() !== 'development',
46
48
  };
47
49
  export class ExtensionManager {
48
50
  options = defaultOptions;
@@ -108,6 +110,7 @@ export class ExtensionManager {
108
110
  * @param {boolean} options.watch - Whether or not to watch the local extensions folder for changes
109
111
  */
110
112
  async initialize(options = {}) {
113
+ const logger = useLogger();
111
114
  this.options = {
112
115
  ...defaultOptions,
113
116
  ...options,
@@ -133,6 +136,7 @@ export class ExtensionManager {
133
136
  * Load all extensions from disk and register them in their respective places
134
137
  */
135
138
  async load() {
139
+ const logger = useLogger();
136
140
  try {
137
141
  await syncExtensions();
138
142
  }
@@ -146,8 +150,7 @@ export class ExtensionManager {
146
150
  this.extensionsSettings = await getExtensionsSettings(this.extensions);
147
151
  }
148
152
  catch (error) {
149
- logger.warn(`Couldn't load extensions`);
150
- logger.warn(error);
153
+ this.handleExtensionError({ error, reason: `Couldn't load extensions` });
151
154
  }
152
155
  await this.registerHooks();
153
156
  await this.registerEndpoints();
@@ -171,12 +174,17 @@ export class ExtensionManager {
171
174
  * Reload all the extensions. Will unload if extensions have already been loaded
172
175
  */
173
176
  reload() {
177
+ if (this.reloadQueue.size > 0) {
178
+ // The pending job in the queue will already handle the additional changes
179
+ return;
180
+ }
181
+ const logger = useLogger();
174
182
  this.reloadQueue.enqueue(async () => {
175
183
  if (this.isLoaded) {
176
- logger.info('Reloading extensions');
177
184
  const prevExtensions = clone(this.extensions);
178
185
  await this.unload();
179
186
  await this.load();
187
+ logger.info('Extensions reloaded');
180
188
  const added = this.extensions.filter((extension) => !prevExtensions.some((prevExtension) => extension.path === prevExtension.path));
181
189
  const removed = prevExtensions.filter((prevExtension) => !this.extensions.some((extension) => prevExtension.path === extension.path));
182
190
  this.updateWatchedExtensions(added, removed);
@@ -231,27 +239,34 @@ export class ExtensionManager {
231
239
  * Start the chokidar watcher for extensions on the local filesystem
232
240
  */
233
241
  initializeWatcher() {
242
+ const logger = useLogger();
234
243
  logger.info('Watching extensions for changes...');
235
- const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
236
- const localExtensionUrls = NESTED_EXTENSION_TYPES.flatMap((type) => {
237
- const typeDir = path.posix.join(extensionDirUrl, pluralize(type));
244
+ const extensionsDir = path.resolve(getExtensionsPath());
245
+ const rootPackageJson = path.resolve(env['PACKAGE_FILE_LOCATION'], 'package.json');
246
+ const localExtensions = path.join(extensionsDir, '*', 'package.json');
247
+ const nestedExtensions = NESTED_EXTENSION_TYPES.flatMap((type) => {
248
+ const typeDir = path.join(extensionsDir, pluralize(type));
238
249
  if (isIn(type, HYBRID_EXTENSION_TYPES)) {
239
250
  return [
240
- path.posix.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
241
- path.posix.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
251
+ path.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
252
+ path.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
242
253
  ];
243
254
  }
244
255
  else {
245
- return path.posix.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
256
+ return path.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
246
257
  }
247
258
  });
248
- this.watcher = chokidar.watch([path.resolve('package.json'), path.posix.join(extensionDirUrl, '*', 'package.json'), ...localExtensionUrls], {
259
+ this.watcher = chokidar.watch([rootPackageJson, localExtensions, ...nestedExtensions], {
249
260
  ignoreInitial: true,
261
+ // dotdirs are watched by default and frequently found in 'node_modules'
262
+ ignored: `${extensionsDir}/**/node_modules/**`,
263
+ // on macOS dotdirs in linked extensions are watched too
264
+ followSymlinks: os.platform() === 'darwin' ? false : true,
250
265
  });
251
266
  this.watcher
252
- .on('add', () => this.reload())
253
- .on('change', () => this.reload())
254
- .on('unlink', () => this.reload());
267
+ .on('add', debounce(() => this.reload(), 500))
268
+ .on('change', debounce(() => this.reload(), 650))
269
+ .on('unlink', debounce(() => this.reload(), 2000));
255
270
  }
256
271
  /**
257
272
  * Close and destroy the local filesystem watcher if enabled
@@ -268,8 +283,12 @@ export class ExtensionManager {
268
283
  */
269
284
  updateWatchedExtensions(added, removed = []) {
270
285
  if (this.watcher) {
286
+ const extensionDir = path.resolve(getExtensionsPath());
287
+ const nestedExtensionDirs = NESTED_EXTENSION_TYPES.map((type) => {
288
+ return path.join(extensionDir, pluralize(type));
289
+ });
271
290
  const toPackageExtensionPaths = (extensions) => extensions
272
- .filter((extension) => !extension.local || extension.type === 'bundle')
291
+ .filter((extension) => !nestedExtensionDirs.some((path) => extension.path.startsWith(path)))
273
292
  .flatMap((extension) => isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
274
293
  ? [
275
294
  path.resolve(extension.path, extension.entrypoint.app),
@@ -287,12 +306,13 @@ export class ExtensionManager {
287
306
  * run.
288
307
  */
289
308
  async generateExtensionBundle() {
309
+ const logger = useLogger();
290
310
  const sharedDepsMapping = await getSharedDepsMapping(APP_SHARED_DEPS);
291
311
  const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
292
312
  find: name,
293
313
  replacement: path,
294
314
  }));
295
- const entrypoint = generateExtensionsEntrypoint(this.extensions);
315
+ const entrypoint = generateExtensionsEntrypoint(this.extensions, this.extensionsSettings);
296
316
  try {
297
317
  const bundle = await rollup({
298
318
  input: 'entry',
@@ -316,15 +336,16 @@ export class ExtensionManager {
316
336
  return null;
317
337
  }
318
338
  async registerSandboxedApiExtension(extension) {
339
+ const logger = useLogger();
319
340
  const sandboxMemory = Number(env['EXTENSIONS_SANDBOX_MEMORY']);
320
341
  const sandboxTimeout = Number(env['EXTENSIONS_SANDBOX_TIMEOUT']);
321
342
  const entrypointPath = path.resolve(extension.path, isTypeIn(extension, HYBRID_EXTENSION_TYPES) ? extension.entrypoint.api : extension.entrypoint);
322
343
  const extensionCode = await readFile(entrypointPath, 'utf-8');
323
344
  const isolate = new ivm.Isolate({
324
345
  memoryLimit: sandboxMemory,
325
- onCatastrophicError: (e) => {
346
+ onCatastrophicError: (error) => {
326
347
  logger.error(`Error in API extension sandbox of ${extension.type} "${extension.name}"`);
327
- logger.error(e);
348
+ logger.error(error);
328
349
  process.abort();
329
350
  },
330
351
  });
@@ -377,8 +398,7 @@ export class ExtensionManager {
377
398
  }
378
399
  }
379
400
  catch (error) {
380
- logger.warn(`Couldn't register hook "${hook.name}"`);
381
- logger.warn(error);
401
+ this.handleExtensionError({ error, reason: `Couldn't register hook "${hook.name}"` });
382
402
  }
383
403
  }
384
404
  }
@@ -410,8 +430,7 @@ export class ExtensionManager {
410
430
  }
411
431
  }
412
432
  catch (error) {
413
- logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
414
- logger.warn(error);
433
+ this.handleExtensionError({ error, reason: `Couldn't register endpoint "${endpoint.name}"` });
415
434
  }
416
435
  }
417
436
  }
@@ -449,8 +468,7 @@ export class ExtensionManager {
449
468
  }
450
469
  }
451
470
  catch (error) {
452
- logger.warn(`Couldn't register operation "${operation.name}"`);
453
- logger.warn(error);
471
+ this.handleExtensionError({ error, reason: `Couldn't register operation "${operation.name}"` });
454
472
  }
455
473
  }
456
474
  }
@@ -460,6 +478,12 @@ export class ExtensionManager {
460
478
  */
461
479
  async registerBundles() {
462
480
  const bundles = this.extensions.filter((extension) => extension.type === 'bundle');
481
+ const extensionEnabled = (extensionName) => {
482
+ const settings = this.extensionsSettings.find(({ name }) => name === extensionName);
483
+ if (!settings)
484
+ return false;
485
+ return settings.enabled;
486
+ };
463
487
  for (const bundle of bundles) {
464
488
  try {
465
489
  const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
@@ -469,14 +493,20 @@ export class ExtensionManager {
469
493
  const configs = getModuleDefault(bundleInstances);
470
494
  const unregisterFunctions = [];
471
495
  for (const { config, name } of configs.hooks) {
496
+ if (!extensionEnabled(`${bundle.name}/${name}`))
497
+ continue;
472
498
  const unregisters = this.registerHook(config, name);
473
499
  unregisterFunctions.push(...unregisters);
474
500
  }
475
501
  for (const { config, name } of configs.endpoints) {
502
+ if (!extensionEnabled(`${bundle.name}/${name}`))
503
+ continue;
476
504
  const unregister = this.registerEndpoint(config, name);
477
505
  unregisterFunctions.push(unregister);
478
506
  }
479
- for (const { config } of configs.operations) {
507
+ for (const { config, name } of configs.operations) {
508
+ if (!extensionEnabled(`${bundle.name}/${name}`))
509
+ continue;
480
510
  const unregister = this.registerOperation(config);
481
511
  unregisterFunctions.push(unregister);
482
512
  }
@@ -486,8 +516,7 @@ export class ExtensionManager {
486
516
  });
487
517
  }
488
518
  catch (error) {
489
- logger.warn(`Couldn't register bundle "${bundle.name}"`);
490
- logger.warn(error);
519
+ this.handleExtensionError({ error, reason: `Couldn't register bundle "${bundle.name}"` });
491
520
  }
492
521
  }
493
522
  }
@@ -495,6 +524,7 @@ export class ExtensionManager {
495
524
  * Register a single hook
496
525
  */
497
526
  registerHook(hookRegistrationCallback, name) {
527
+ const logger = useLogger();
498
528
  let scheduleIndex = 0;
499
529
  const unregisterFunctions = [];
500
530
  const hookRegistrationContext = {
@@ -534,7 +564,7 @@ export class ExtensionManager {
534
564
  });
535
565
  }
536
566
  else {
537
- logger.warn(`Couldn't register cron hook. Provided cron is invalid: ${cron}`);
567
+ this.handleExtensionError({ reason: `Couldn't register cron hook. Provided cron is invalid: ${cron}` });
538
568
  }
539
569
  },
540
570
  embed: (position, code) => {
@@ -556,7 +586,7 @@ export class ExtensionManager {
556
586
  }
557
587
  }
558
588
  else {
559
- logger.warn(`Couldn't register embed hook. Provided code is empty!`);
589
+ this.handleExtensionError({ reason: `Couldn't register embed hook. Provided code is empty!` });
560
590
  }
561
591
  },
562
592
  };
@@ -574,6 +604,7 @@ export class ExtensionManager {
574
604
  * Register an individual endpoint
575
605
  */
576
606
  registerEndpoint(config, name) {
607
+ const logger = useLogger();
577
608
  const endpointRegistrationCallback = typeof config === 'function' ? config : config.handler;
578
609
  const nameWithoutType = name.includes(':') ? name.split(':')[0] : name;
579
610
  const routeName = typeof config === 'function' ? nameWithoutType : config.id;
@@ -610,4 +641,23 @@ export class ExtensionManager {
610
641
  const unregisterFunctions = Array.from(this.unregisterFunctionMap.values());
611
642
  await Promise.all(unregisterFunctions.map((fn) => fn()));
612
643
  }
644
+ /**
645
+ * If extensions must load successfully, any errors will cause the process to exit.
646
+ * Otherwise, the error will only be logged as a warning.
647
+ */
648
+ handleExtensionError({ error, reason }) {
649
+ const logger = useLogger();
650
+ if (toBoolean(env['EXTENSIONS_MUST_LOAD'])) {
651
+ logger.error('EXTENSION_MUST_LOAD is enabled and an extension failed to load.');
652
+ logger.error(reason);
653
+ if (error)
654
+ logger.error(error);
655
+ process.exit(1);
656
+ }
657
+ else {
658
+ logger.warn(reason);
659
+ if (error)
660
+ logger.warn(error);
661
+ }
662
+ }
613
663
  }