@directus/api 17.1.0 → 18.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 (95) hide show
  1. package/dist/app.js +8 -2
  2. package/dist/auth/drivers/ldap.js +14 -16
  3. package/dist/auth/drivers/local.js +16 -10
  4. package/dist/auth/drivers/oauth2.js +16 -11
  5. package/dist/auth/drivers/openid.js +16 -11
  6. package/dist/auth/drivers/saml.js +27 -12
  7. package/dist/cli/commands/init/index.js +3 -3
  8. package/dist/cli/commands/security/key.js +2 -2
  9. package/dist/cli/utils/create-env/env-stub.liquid +19 -4
  10. package/dist/cli/utils/create-env/index.js +2 -2
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.js +11 -4
  13. package/dist/controllers/auth.js +54 -19
  14. package/dist/controllers/extensions.js +102 -5
  15. package/dist/controllers/items.js +3 -2
  16. package/dist/controllers/permissions.js +1 -1
  17. package/dist/controllers/shares.js +19 -4
  18. package/dist/database/migrations/20220429A-add-flows.js +3 -3
  19. package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
  20. package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
  21. package/dist/database/migrations/20240204A-marketplace.js +68 -0
  22. package/dist/database/migrations/run.js +3 -2
  23. package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
  24. package/dist/extensions/lib/get-extensions-settings.js +70 -22
  25. package/dist/extensions/lib/get-extensions.d.ts +5 -1
  26. package/dist/extensions/lib/get-extensions.js +7 -31
  27. package/dist/extensions/lib/installation/index.d.ts +2 -0
  28. package/dist/extensions/lib/installation/index.js +9 -0
  29. package/dist/extensions/lib/installation/manager.d.ts +5 -0
  30. package/dist/extensions/lib/installation/manager.js +90 -0
  31. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
  32. package/dist/extensions/lib/sync-extensions.js +11 -10
  33. package/dist/extensions/manager.d.ts +27 -25
  34. package/dist/extensions/manager.js +214 -183
  35. package/dist/middleware/authenticate.d.ts +1 -0
  36. package/dist/middleware/error-handler.js +22 -18
  37. package/dist/middleware/extract-token.d.ts +6 -5
  38. package/dist/middleware/extract-token.js +27 -11
  39. package/dist/middleware/merge-content-versions.d.ts +2 -0
  40. package/dist/middleware/merge-content-versions.js +26 -0
  41. package/dist/middleware/respond.js +0 -12
  42. package/dist/middleware/validate-batch.d.ts +1 -0
  43. package/dist/request/agent-with-ip-validation.d.ts +1 -1
  44. package/dist/request/agent-with-ip-validation.js +5 -1
  45. package/dist/services/activity.js +3 -3
  46. package/dist/services/assets.js +2 -3
  47. package/dist/services/authentication.d.ts +7 -2
  48. package/dist/services/authentication.js +21 -13
  49. package/dist/services/extensions.d.ts +4 -8
  50. package/dist/services/extensions.js +110 -93
  51. package/dist/services/fields.js +28 -22
  52. package/dist/services/graphql/index.js +98 -42
  53. package/dist/services/index.d.ts +1 -1
  54. package/dist/services/index.js +1 -1
  55. package/dist/services/mail/index.d.ts +1 -1
  56. package/dist/services/mail/index.js +4 -2
  57. package/dist/services/payload.js +2 -2
  58. package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
  59. package/dist/services/{permissions.js → permissions/index.js} +6 -23
  60. package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
  61. package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
  62. package/dist/services/relations.d.ts +2 -3
  63. package/dist/services/relations.js +2 -2
  64. package/dist/services/roles.js +1 -1
  65. package/dist/services/server.js +3 -0
  66. package/dist/services/shares.d.ts +3 -1
  67. package/dist/services/shares.js +9 -5
  68. package/dist/storage/index.js +5 -4
  69. package/dist/types/auth.d.ts +6 -4
  70. package/dist/types/graphql.d.ts +1 -0
  71. package/dist/utils/apply-query.js +3 -3
  72. package/dist/utils/filter-items.d.ts +2 -2
  73. package/dist/utils/filter-items.js +1 -3
  74. package/dist/utils/get-cache-headers.d.ts +1 -0
  75. package/dist/utils/get-cache-key.d.ts +1 -0
  76. package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
  77. package/dist/utils/get-ip-from-req.d.ts +1 -0
  78. package/dist/utils/get-milliseconds.d.ts +1 -1
  79. package/dist/utils/get-milliseconds.js +4 -1
  80. package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
  81. package/dist/utils/is-login-redirect-allowed.js +34 -0
  82. package/dist/utils/is-url-allowed.d.ts +1 -1
  83. package/dist/utils/is-url-allowed.js +5 -5
  84. package/dist/utils/is-valid-uuid.d.ts +3 -0
  85. package/dist/utils/is-valid-uuid.js +21 -0
  86. package/dist/utils/jwt.d.ts +1 -1
  87. package/dist/utils/jwt.js +3 -3
  88. package/dist/utils/merge-version-data.d.ts +3 -0
  89. package/dist/utils/merge-version-data.js +134 -0
  90. package/dist/utils/sanitize-query.js +2 -0
  91. package/dist/utils/should-skip-cache.d.ts +1 -0
  92. package/dist/utils/validate-keys.js +2 -2
  93. package/dist/utils/validate-query.js +1 -0
  94. package/dist/websocket/controllers/base.js +2 -2
  95. package/package.json +44 -45
@@ -1,22 +1,22 @@
1
- import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
2
1
  import { useEnv } from '@directus/env';
3
- import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES, NESTED_EXTENSION_TYPES } from '@directus/extensions';
2
+ import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES } from '@directus/extensions';
4
3
  import { generateExtensionsEntrypoint } from '@directus/extensions/node';
5
- import { isIn, isTypeIn, pluralize, toBoolean } from '@directus/utils';
6
- import { getNodeEnv } from '@directus/utils/node';
4
+ import { isTypeIn, toBoolean } from '@directus/utils';
5
+ import { getNodeEnv, pathToRelativeUrl, processId } from '@directus/utils/node';
7
6
  import aliasDefault from '@rollup/plugin-alias';
8
7
  import nodeResolveDefault from '@rollup/plugin-node-resolve';
9
8
  import virtualDefault from '@rollup/plugin-virtual';
10
9
  import chokidar, { FSWatcher } from 'chokidar';
11
10
  import express, { Router } from 'express';
12
11
  import ivm from 'isolated-vm';
13
- import { clone, debounce } from 'lodash-es';
12
+ import { clone, debounce, isPlainObject } from 'lodash-es';
14
13
  import { readFile, readdir } from 'node:fs/promises';
15
14
  import os from 'node:os';
16
15
  import { dirname } from 'node:path';
17
16
  import { fileURLToPath } from 'node:url';
18
17
  import path from 'path';
19
18
  import { rollup } from 'rollup';
19
+ import { useBus } from '../bus/index.js';
20
20
  import getDatabase from '../database/index.js';
21
21
  import emitter, { Emitter } from '../emitter.js';
22
22
  import { getFlowManager } from '../flows.js';
@@ -32,6 +32,7 @@ import { getExtensionsPath } from './lib/get-extensions-path.js';
32
32
  import { getExtensionsSettings } from './lib/get-extensions-settings.js';
33
33
  import { getExtensions } from './lib/get-extensions.js';
34
34
  import { getSharedDepsMapping } from './lib/get-shared-deps-mapping.js';
35
+ import { getInstallationManager } from './lib/installation/index.js';
35
36
  import { generateApiExtensionsSandboxEntrypoint } from './lib/sandbox/generate-api-extensions-sandbox-entrypoint.js';
36
37
  import { instantiateSandboxSdk } from './lib/sandbox/sdk/instantiate.js';
37
38
  import { syncExtensions } from './lib/sync-extensions.js';
@@ -52,10 +53,12 @@ export class ExtensionManager {
52
53
  * Whether or not the extensions have been read from disk and registered into the system
53
54
  */
54
55
  isLoaded = false;
55
- /**
56
- * All extensions that are loaded within the current process
57
- */
58
- extensions = [];
56
+ // folder:Extension
57
+ localExtensions = new Map();
58
+ // versionId:Extension
59
+ registryExtensions = new Map();
60
+ // name:Extension
61
+ moduleExtensions = new Map();
59
62
  /**
60
63
  * Settings for the extensions that are loaded within the current process
61
64
  */
@@ -102,6 +105,30 @@ export class ExtensionManager {
102
105
  * Optional file system watcher to auto-reload extensions when the local file system changes
103
106
  */
104
107
  watcher = null;
108
+ /**
109
+ * installation manager responsible for installing extensions from registries
110
+ */
111
+ installationManager = getInstallationManager();
112
+ messenger = useBus();
113
+ /**
114
+ * channel to publish on registering extension from external registry
115
+ */
116
+ reloadChannel = `extensions.reload`;
117
+ processId = processId();
118
+ get extensions() {
119
+ return [...this.localExtensions.values(), ...this.registryExtensions.values(), ...this.moduleExtensions.values()];
120
+ }
121
+ getExtension(source, folder) {
122
+ switch (source) {
123
+ case 'module':
124
+ return this.moduleExtensions.get(folder);
125
+ case 'registry':
126
+ return this.registryExtensions.get(folder);
127
+ case 'local':
128
+ return this.localExtensions.get(folder);
129
+ }
130
+ return undefined;
131
+ }
105
132
  /**
106
133
  * Load and register all extensions
107
134
  *
@@ -129,33 +156,54 @@ export class ExtensionManager {
129
156
  }
130
157
  }
131
158
  if (this.options.watch && !wasWatcherInitialized) {
132
- this.updateWatchedExtensions(this.extensions);
159
+ this.updateWatchedExtensions(Array.from(this.localExtensions.values()));
133
160
  }
161
+ this.messenger.subscribe(this.reloadChannel, (payload) => {
162
+ // Ignore requests for reloading that were published by the current process
163
+ if (isPlainObject(payload) && 'origin' in payload && payload['origin'] === this.processId)
164
+ return;
165
+ this.reload();
166
+ });
167
+ }
168
+ /**
169
+ * Installs an external extension from registry
170
+ */
171
+ async install(versionId) {
172
+ await this.installationManager.install(versionId);
173
+ await this.reload();
174
+ await this.messenger.publish(this.reloadChannel, { origin: this.processId });
175
+ }
176
+ async uninstall(folder) {
177
+ await this.installationManager.uninstall(folder);
178
+ await this.reload();
179
+ await this.messenger.publish(this.reloadChannel, { origin: this.processId });
134
180
  }
135
181
  /**
136
182
  * Load all extensions from disk and register them in their respective places
137
183
  */
138
184
  async load() {
139
185
  const logger = useLogger();
140
- try {
141
- await syncExtensions();
142
- }
143
- catch (error) {
144
- logger.error(`Failed to sync extensions`);
145
- logger.error(error);
146
- process.exit(1);
186
+ if (env['EXTENSIONS_LOCATION']) {
187
+ try {
188
+ await syncExtensions();
189
+ }
190
+ catch (error) {
191
+ logger.error(`Failed to sync extensions`);
192
+ logger.error(error);
193
+ process.exit(1);
194
+ }
147
195
  }
148
196
  try {
149
- this.extensions = await getExtensions();
150
- this.extensionsSettings = await getExtensionsSettings(this.extensions);
197
+ const { local, registry, module } = await getExtensions();
198
+ this.localExtensions = local;
199
+ this.registryExtensions = registry;
200
+ this.moduleExtensions = module;
201
+ this.extensionsSettings = await getExtensionsSettings({ local, registry, module });
151
202
  }
152
203
  catch (error) {
153
204
  this.handleExtensionError({ error, reason: `Couldn't load extensions` });
154
205
  }
155
- await this.registerHooks();
156
- await this.registerEndpoints();
157
- await this.registerOperations();
158
- await this.registerBundles();
206
+ await Promise.all([this.registerInternalOperations(), this.registerApiExtensions()]);
159
207
  if (env['SERVE_APP']) {
160
208
  this.appExtensionsBundle = await this.generateExtensionBundle();
161
209
  }
@@ -176,9 +224,15 @@ export class ExtensionManager {
176
224
  reload() {
177
225
  if (this.reloadQueue.size > 0) {
178
226
  // The pending job in the queue will already handle the additional changes
179
- return;
227
+ return Promise.resolve();
180
228
  }
181
229
  const logger = useLogger();
230
+ let resolve;
231
+ let reject;
232
+ const promise = new Promise((res, rej) => {
233
+ resolve = res;
234
+ reject = rej;
235
+ });
182
236
  this.reloadQueue.enqueue(async () => {
183
237
  if (this.isLoaded) {
184
238
  const prevExtensions = clone(this.extensions);
@@ -196,11 +250,14 @@ export class ExtensionManager {
196
250
  if (removedExtensions.length > 0) {
197
251
  logger.info(`Removed extensions: ${removedExtensions.join(', ')}`);
198
252
  }
253
+ resolve();
199
254
  }
200
255
  else {
201
256
  logger.warn('Extensions have to be loaded before they can be reloaded');
257
+ reject(new Error('Extensions have to be loaded before they can be reloaded'));
202
258
  }
203
259
  });
260
+ return promise;
204
261
  }
205
262
  /**
206
263
  * Return the previously generated app extensions bundle
@@ -229,37 +286,16 @@ export class ExtensionManager {
229
286
  body: wrapEmbeds('Custom Embed Body', this.hookEmbedsBody),
230
287
  };
231
288
  }
232
- /**
233
- * Allow reading the installed extensions
234
- */
235
- getExtensions() {
236
- return this.extensions;
237
- }
238
289
  /**
239
290
  * Start the chokidar watcher for extensions on the local filesystem
240
291
  */
241
292
  initializeWatcher() {
242
293
  const logger = useLogger();
243
294
  logger.info('Watching extensions for changes...');
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));
249
- if (isIn(type, HYBRID_EXTENSION_TYPES)) {
250
- return [
251
- path.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
252
- path.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
253
- ];
254
- }
255
- else {
256
- return path.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
257
- }
258
- });
259
- this.watcher = chokidar.watch([rootPackageJson, localExtensions, ...nestedExtensions], {
260
- ignoreInitial: true,
261
- // dotdirs are watched by default and frequently found in 'node_modules'
262
- ignored: `${extensionsDir}/**/node_modules/**`,
295
+ const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
296
+ this.watcher = chokidar.watch([path.resolve('package.json'), path.posix.join(extensionDirUrl, '*', 'package.json')], {
297
+ ignoreInitial: true, // dotdirs are watched by default and frequently found in 'node_modules'
298
+ ignored: `${extensionDirUrl}/**/node_modules/**`,
263
299
  // on macOS dotdirs in linked extensions are watched too
264
300
  followSymlinks: os.platform() === 'darwin' ? false : true,
265
301
  });
@@ -283,12 +319,8 @@ export class ExtensionManager {
283
319
  */
284
320
  updateWatchedExtensions(added, removed = []) {
285
321
  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
- });
290
322
  const toPackageExtensionPaths = (extensions) => extensions
291
- .filter((extension) => !nestedExtensionDirs.some((path) => extension.path.startsWith(path)))
323
+ .filter((extension) => !extension.local || extension.type === 'bundle')
292
324
  .flatMap((extension) => isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
293
325
  ? [
294
326
  path.resolve(extension.path, extension.entrypoint.app),
@@ -312,7 +344,7 @@ export class ExtensionManager {
312
344
  find: name,
313
345
  replacement: path,
314
346
  }));
315
- const entrypoint = generateExtensionsEntrypoint(this.extensions, this.extensionsSettings);
347
+ const entrypoint = generateExtensionsEntrypoint({ module: this.moduleExtensions, registry: this.registryExtensions, local: this.localExtensions }, this.extensionsSettings);
316
348
  try {
317
349
  const bundle = await rollup({
318
350
  input: 'entry',
@@ -370,154 +402,153 @@ export class ExtensionManager {
370
402
  isolate.dispose();
371
403
  });
372
404
  }
373
- /**
374
- * Import the hook module code for all hook extensions, and register them individually through
375
- * registerHook
376
- */
377
- async registerHooks() {
378
- const hooks = this.extensions.filter((extension) => extension.type === 'hook');
379
- for (const hook of hooks) {
380
- const { enabled } = this.extensionsSettings.find(({ name }) => name === hook.name) ?? { enabled: false };
381
- if (!enabled)
382
- continue;
383
- try {
384
- if (hook.sandbox?.enabled) {
385
- await this.registerSandboxedApiExtension(hook);
386
- }
387
- else {
388
- const hookPath = path.resolve(hook.path, hook.entrypoint);
389
- const hookInstance = await importFileUrl(hookPath, import.meta.url, {
390
- fresh: true,
391
- });
392
- const config = getModuleDefault(hookInstance);
393
- const unregisterFunctions = this.registerHook(config, hook.name);
394
- this.unregisterFunctionMap.set(hook.name, async () => {
395
- await Promise.all(unregisterFunctions.map((fn) => fn()));
396
- deleteFromRequireCache(hookPath);
397
- });
405
+ async registerApiExtensions() {
406
+ const sources = {
407
+ module: this.moduleExtensions,
408
+ registry: this.registryExtensions,
409
+ local: this.localExtensions,
410
+ };
411
+ await Promise.all(Object.entries(sources).map(async ([source, extensions]) => {
412
+ await Promise.all(Array.from(extensions.entries()).map(async ([folder, extension]) => {
413
+ const { id, enabled } = this.extensionsSettings.find((settings) => settings.source === source && settings.folder === folder) ?? { enabled: false };
414
+ if (!enabled)
415
+ return;
416
+ switch (extension.type) {
417
+ case 'hook':
418
+ await this.registerHookExtension(extension);
419
+ break;
420
+ case 'endpoint':
421
+ await this.registerEndpointExtension(extension);
422
+ break;
423
+ case 'operation':
424
+ await this.registerOperationExtension(extension);
425
+ break;
426
+ case 'bundle':
427
+ await this.registerBundleExtension(extension, source, id);
428
+ break;
429
+ default:
430
+ return;
398
431
  }
432
+ }));
433
+ }));
434
+ }
435
+ async registerHookExtension(hook) {
436
+ try {
437
+ if (hook.sandbox?.enabled) {
438
+ await this.registerSandboxedApiExtension(hook);
399
439
  }
400
- catch (error) {
401
- this.handleExtensionError({ error, reason: `Couldn't register hook "${hook.name}"` });
440
+ else {
441
+ const hookPath = path.resolve(hook.path, hook.entrypoint);
442
+ const hookInstance = await importFileUrl(hookPath, import.meta.url, {
443
+ fresh: true,
444
+ });
445
+ const config = getModuleDefault(hookInstance);
446
+ const unregisterFunctions = this.registerHook(config, hook.name);
447
+ this.unregisterFunctionMap.set(hook.name, async () => {
448
+ await Promise.all(unregisterFunctions.map((fn) => fn()));
449
+ deleteFromRequireCache(hookPath);
450
+ });
402
451
  }
403
452
  }
453
+ catch (error) {
454
+ this.handleExtensionError({ error, reason: `Couldn't register hook "${hook.name}"` });
455
+ }
404
456
  }
405
- /**
406
- * Import the endpoint module code for all endpoint extensions, and register them individually through
407
- * registerEndpoint
408
- */
409
- async registerEndpoints() {
410
- const endpoints = this.extensions.filter((extension) => extension.type === 'endpoint');
411
- for (const endpoint of endpoints) {
412
- const { enabled } = this.extensionsSettings.find(({ name }) => name === endpoint.name) ?? { enabled: false };
413
- if (!enabled)
414
- continue;
415
- try {
416
- if (endpoint.sandbox?.enabled) {
417
- await this.registerSandboxedApiExtension(endpoint);
418
- }
419
- else {
420
- const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint);
421
- const endpointInstance = await importFileUrl(endpointPath, import.meta.url, {
422
- fresh: true,
423
- });
424
- const config = getModuleDefault(endpointInstance);
425
- const unregister = this.registerEndpoint(config, endpoint.name);
426
- this.unregisterFunctionMap.set(endpoint.name, async () => {
427
- await unregister();
428
- deleteFromRequireCache(endpointPath);
429
- });
430
- }
457
+ async registerEndpointExtension(endpoint) {
458
+ try {
459
+ if (endpoint.sandbox?.enabled) {
460
+ await this.registerSandboxedApiExtension(endpoint);
431
461
  }
432
- catch (error) {
433
- this.handleExtensionError({ error, reason: `Couldn't register endpoint "${endpoint.name}"` });
462
+ else {
463
+ const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint);
464
+ const endpointInstance = await importFileUrl(endpointPath, import.meta.url, {
465
+ fresh: true,
466
+ });
467
+ const config = getModuleDefault(endpointInstance);
468
+ const unregister = this.registerEndpoint(config, endpoint.name);
469
+ this.unregisterFunctionMap.set(endpoint.name, async () => {
470
+ await unregister();
471
+ deleteFromRequireCache(endpointPath);
472
+ });
434
473
  }
435
474
  }
436
- }
437
- /**
438
- * Import the operation module code for all operation extensions, and register them individually through
439
- * registerOperation
440
- */
441
- async registerOperations() {
442
- const internalOperations = await readdir(path.join(__dirname, '..', 'operations'));
443
- for (const operation of internalOperations) {
444
- const operationInstance = await import(`../operations/${operation}/index.js`);
445
- const config = getModuleDefault(operationInstance);
446
- this.registerOperation(config);
475
+ catch (error) {
476
+ this.handleExtensionError({ error, reason: `Couldn't register endpoint "${endpoint.name}"` });
447
477
  }
448
- const operations = this.extensions.filter((extension) => extension.type === 'operation');
449
- for (const operation of operations) {
450
- const { enabled } = this.extensionsSettings.find(({ name }) => name === operation.name) ?? { enabled: false };
451
- if (!enabled)
452
- continue;
453
- try {
454
- if (operation.sandbox?.enabled) {
455
- await this.registerSandboxedApiExtension(operation);
456
- }
457
- else {
458
- const operationPath = path.resolve(operation.path, operation.entrypoint.api);
459
- const operationInstance = await importFileUrl(operationPath, import.meta.url, {
460
- fresh: true,
461
- });
462
- const config = getModuleDefault(operationInstance);
463
- const unregister = this.registerOperation(config);
464
- this.unregisterFunctionMap.set(operation.name, async () => {
465
- await unregister();
466
- deleteFromRequireCache(operationPath);
467
- });
468
- }
478
+ }
479
+ async registerOperationExtension(operation) {
480
+ try {
481
+ if (operation.sandbox?.enabled) {
482
+ await this.registerSandboxedApiExtension(operation);
469
483
  }
470
- catch (error) {
471
- this.handleExtensionError({ error, reason: `Couldn't register operation "${operation.name}"` });
484
+ else {
485
+ const operationPath = path.resolve(operation.path, operation.entrypoint.api);
486
+ const operationInstance = await importFileUrl(operationPath, import.meta.url, {
487
+ fresh: true,
488
+ });
489
+ const config = getModuleDefault(operationInstance);
490
+ const unregister = this.registerOperation(config);
491
+ this.unregisterFunctionMap.set(operation.name, async () => {
492
+ await unregister();
493
+ deleteFromRequireCache(operationPath);
494
+ });
472
495
  }
473
496
  }
497
+ catch (error) {
498
+ this.handleExtensionError({ error, reason: `Couldn't register operation "${operation.name}"` });
499
+ }
474
500
  }
475
- /**
476
- * Import the module code for all hook, endpoint, and operation extensions registered within a
477
- * bundle, and register them with their respective registration function
478
- */
479
- async registerBundles() {
480
- const bundles = this.extensions.filter((extension) => extension.type === 'bundle');
501
+ async registerBundleExtension(bundle, source, bundleId) {
481
502
  const extensionEnabled = (extensionName) => {
482
- const settings = this.extensionsSettings.find(({ name }) => name === extensionName);
503
+ const settings = this.extensionsSettings.find((settings) => settings.source === source && settings.folder === extensionName && settings.bundle === bundleId);
483
504
  if (!settings)
484
505
  return false;
485
506
  return settings.enabled;
486
507
  };
487
- for (const bundle of bundles) {
488
- try {
489
- const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
490
- const bundleInstances = await importFileUrl(bundlePath, import.meta.url, {
491
- fresh: true,
492
- });
493
- const configs = getModuleDefault(bundleInstances);
494
- const unregisterFunctions = [];
495
- for (const { config, name } of configs.hooks) {
496
- if (!extensionEnabled(`${bundle.name}/${name}`))
497
- continue;
498
- const unregisters = this.registerHook(config, name);
499
- unregisterFunctions.push(...unregisters);
500
- }
501
- for (const { config, name } of configs.endpoints) {
502
- if (!extensionEnabled(`${bundle.name}/${name}`))
503
- continue;
504
- const unregister = this.registerEndpoint(config, name);
505
- unregisterFunctions.push(unregister);
506
- }
507
- for (const { config, name } of configs.operations) {
508
- if (!extensionEnabled(`${bundle.name}/${name}`))
509
- continue;
510
- const unregister = this.registerOperation(config);
511
- unregisterFunctions.push(unregister);
512
- }
513
- this.unregisterFunctionMap.set(bundle.name, async () => {
514
- await Promise.all(unregisterFunctions.map((fn) => fn()));
515
- deleteFromRequireCache(bundlePath);
516
- });
508
+ try {
509
+ const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
510
+ const bundleInstances = await importFileUrl(bundlePath, import.meta.url, {
511
+ fresh: true,
512
+ });
513
+ const configs = getModuleDefault(bundleInstances);
514
+ const unregisterFunctions = [];
515
+ for (const { config, name } of configs.hooks) {
516
+ if (!extensionEnabled(name))
517
+ continue;
518
+ const unregisters = this.registerHook(config, name);
519
+ unregisterFunctions.push(...unregisters);
517
520
  }
518
- catch (error) {
519
- this.handleExtensionError({ error, reason: `Couldn't register bundle "${bundle.name}"` });
521
+ for (const { config, name } of configs.endpoints) {
522
+ if (!extensionEnabled(name))
523
+ continue;
524
+ const unregister = this.registerEndpoint(config, name);
525
+ unregisterFunctions.push(unregister);
526
+ }
527
+ for (const { config, name } of configs.operations) {
528
+ if (!extensionEnabled(name))
529
+ continue;
530
+ const unregister = this.registerOperation(config);
531
+ unregisterFunctions.push(unregister);
520
532
  }
533
+ this.unregisterFunctionMap.set(bundle.name, async () => {
534
+ await Promise.all(unregisterFunctions.map((fn) => fn()));
535
+ deleteFromRequireCache(bundlePath);
536
+ });
537
+ }
538
+ catch (error) {
539
+ this.handleExtensionError({ error, reason: `Couldn't register bundle "${bundle.name}"` });
540
+ }
541
+ }
542
+ /**
543
+ * Import the operation module code for all operation extensions, and register them individually through
544
+ * registerOperation
545
+ */
546
+ async registerInternalOperations() {
547
+ const internalOperations = await readdir(path.join(__dirname, '..', 'operations'));
548
+ for (const operation of internalOperations) {
549
+ const operationInstance = await import(`../operations/${operation}/index.js`);
550
+ const config = getModuleDefault(operationInstance);
551
+ this.registerOperation(config);
521
552
  }
522
553
  }
523
554
  /**
@@ -1,4 +1,5 @@
1
1
  /// <reference types="qs" />
2
+ /// <reference types="cookie-parser" />
2
3
  import type { NextFunction, Request, Response } from 'express';
3
4
  /**
4
5
  * Verify the passed JWT and assign the user ID and role to `req`
@@ -1,5 +1,5 @@
1
1
  import { ErrorCode, MethodNotAllowedError, isDirectusError } from '@directus/errors';
2
- import { toArray } from '@directus/utils';
2
+ import { isObject, toArray } from '@directus/utils';
3
3
  import { getNodeEnv } from '@directus/utils/node';
4
4
  import getDatabase from '../database/index.js';
5
5
  import emitter from '../emitter.js';
@@ -13,43 +13,47 @@ const errorHandler = (err, req, res, _next) => {
13
13
  };
14
14
  const errors = toArray(err);
15
15
  let status = null;
16
- for (const err of errors) {
16
+ for (const error of errors) {
17
17
  if (getNodeEnv() === 'development') {
18
- err.extensions = {
19
- ...(err.extensions || {}),
20
- stack: err.stack,
21
- };
18
+ if (isObject(error)) {
19
+ error['extensions'] = {
20
+ ...(error['extensions'] || {}),
21
+ stack: error['stack'],
22
+ };
23
+ }
22
24
  }
23
- if (isDirectusError(err)) {
24
- logger.debug(err);
25
+ if (isDirectusError(error)) {
26
+ logger.debug(error);
25
27
  if (!status) {
26
- status = err.status;
28
+ status = error.status;
27
29
  }
28
- else if (status !== err.status) {
30
+ else if (status !== error.status) {
29
31
  status = 500;
30
32
  }
31
33
  payload.errors.push({
32
- message: err.message,
34
+ message: error.message,
33
35
  extensions: {
34
- code: err.code,
35
- ...(err.extensions ?? {}),
36
+ code: error.code,
37
+ ...(error.extensions ?? {}),
36
38
  },
37
39
  });
38
- if (isDirectusError(err, ErrorCode.MethodNotAllowed)) {
39
- res.header('Allow', err.extensions.allowed.join(', '));
40
+ if (isDirectusError(error, ErrorCode.MethodNotAllowed)) {
41
+ res.header('Allow', error.extensions.allowed.join(', '));
40
42
  }
41
43
  }
42
44
  else {
43
- logger.error(err);
45
+ logger.error(error);
44
46
  status = 500;
45
47
  if (req.accountability?.admin === true) {
48
+ const localError = isObject(error) ? error : {};
49
+ const message = localError['message'] ?? typeof error === 'string' ? error : null;
46
50
  payload = {
47
51
  errors: [
48
52
  {
49
- message: err.message,
53
+ message: message || 'An unexpected error occurred.',
50
54
  extensions: {
51
55
  code: 'INTERNAL_SERVER_ERROR',
52
- ...err.extensions,
56
+ ...(localError['extensions'] ?? {}),
53
57
  },
54
58
  },
55
59
  ],
@@ -1,11 +1,12 @@
1
+ import type { RequestHandler } from 'express';
1
2
  /**
2
- * Extract access token from:
3
+ * Extract access token from
3
4
  *
4
- * Authorization: Bearer
5
- * access_token query parameter
5
+ * - 'access_token' query parameter
6
+ * - 'Authorization' header
7
+ * - Session cookie
6
8
  *
7
- * and store in req.token
9
+ * and store it under req.token
8
10
  */
9
- import type { RequestHandler } from 'express';
10
11
  declare const extractToken: RequestHandler;
11
12
  export default extractToken;