@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.
- package/dist/app.js +8 -2
- package/dist/auth/drivers/ldap.js +14 -16
- package/dist/auth/drivers/local.js +16 -10
- package/dist/auth/drivers/oauth2.js +16 -11
- package/dist/auth/drivers/openid.js +16 -11
- package/dist/auth/drivers/saml.js +27 -12
- package/dist/cli/commands/init/index.js +3 -3
- package/dist/cli/commands/security/key.js +2 -2
- package/dist/cli/utils/create-env/env-stub.liquid +19 -4
- package/dist/cli/utils/create-env/index.js +2 -2
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +11 -4
- package/dist/controllers/auth.js +54 -19
- package/dist/controllers/extensions.js +102 -5
- package/dist/controllers/items.js +3 -2
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/shares.js +19 -4
- package/dist/database/migrations/20220429A-add-flows.js +3 -3
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
- package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
- package/dist/database/migrations/20240204A-marketplace.js +68 -0
- package/dist/database/migrations/run.js +3 -2
- package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
- package/dist/extensions/lib/get-extensions-settings.js +70 -22
- package/dist/extensions/lib/get-extensions.d.ts +5 -1
- package/dist/extensions/lib/get-extensions.js +7 -31
- package/dist/extensions/lib/installation/index.d.ts +2 -0
- package/dist/extensions/lib/installation/index.js +9 -0
- package/dist/extensions/lib/installation/manager.d.ts +5 -0
- package/dist/extensions/lib/installation/manager.js +90 -0
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
- package/dist/extensions/lib/sync-extensions.js +11 -10
- package/dist/extensions/manager.d.ts +27 -25
- package/dist/extensions/manager.js +214 -183
- package/dist/middleware/authenticate.d.ts +1 -0
- package/dist/middleware/error-handler.js +22 -18
- package/dist/middleware/extract-token.d.ts +6 -5
- package/dist/middleware/extract-token.js +27 -11
- package/dist/middleware/merge-content-versions.d.ts +2 -0
- package/dist/middleware/merge-content-versions.js +26 -0
- package/dist/middleware/respond.js +0 -12
- package/dist/middleware/validate-batch.d.ts +1 -0
- package/dist/request/agent-with-ip-validation.d.ts +1 -1
- package/dist/request/agent-with-ip-validation.js +5 -1
- package/dist/services/activity.js +3 -3
- package/dist/services/assets.js +2 -3
- package/dist/services/authentication.d.ts +7 -2
- package/dist/services/authentication.js +21 -13
- package/dist/services/extensions.d.ts +4 -8
- package/dist/services/extensions.js +110 -93
- package/dist/services/fields.js +28 -22
- package/dist/services/graphql/index.js +98 -42
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/mail/index.d.ts +1 -1
- package/dist/services/mail/index.js +4 -2
- package/dist/services/payload.js +2 -2
- package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
- package/dist/services/{permissions.js → permissions/index.js} +6 -23
- package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
- package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
- package/dist/services/relations.d.ts +2 -3
- package/dist/services/relations.js +2 -2
- package/dist/services/roles.js +1 -1
- package/dist/services/server.js +3 -0
- package/dist/services/shares.d.ts +3 -1
- package/dist/services/shares.js +9 -5
- package/dist/storage/index.js +5 -4
- package/dist/types/auth.d.ts +6 -4
- package/dist/types/graphql.d.ts +1 -0
- package/dist/utils/apply-query.js +3 -3
- package/dist/utils/filter-items.d.ts +2 -2
- package/dist/utils/filter-items.js +1 -3
- package/dist/utils/get-cache-headers.d.ts +1 -0
- package/dist/utils/get-cache-key.d.ts +1 -0
- package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
- package/dist/utils/get-ip-from-req.d.ts +1 -0
- package/dist/utils/get-milliseconds.d.ts +1 -1
- package/dist/utils/get-milliseconds.js +4 -1
- package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
- package/dist/utils/is-login-redirect-allowed.js +34 -0
- package/dist/utils/is-url-allowed.d.ts +1 -1
- package/dist/utils/is-url-allowed.js +5 -5
- package/dist/utils/is-valid-uuid.d.ts +3 -0
- package/dist/utils/is-valid-uuid.js +21 -0
- package/dist/utils/jwt.d.ts +1 -1
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/merge-version-data.d.ts +3 -0
- package/dist/utils/merge-version-data.js +134 -0
- package/dist/utils/sanitize-query.js +2 -0
- package/dist/utils/should-skip-cache.d.ts +1 -0
- package/dist/utils/validate-keys.js +2 -2
- package/dist/utils/validate-query.js +1 -0
- package/dist/websocket/controllers/base.js +2 -2
- 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
|
|
2
|
+
import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES } from '@directus/extensions';
|
|
4
3
|
import { generateExtensionsEntrypoint } from '@directus/extensions/node';
|
|
5
|
-
import {
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
this.
|
|
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.
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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) => !
|
|
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.
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
await
|
|
396
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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((
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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,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
|
|
16
|
+
for (const error of errors) {
|
|
17
17
|
if (getNodeEnv() === 'development') {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (isObject(error)) {
|
|
19
|
+
error['extensions'] = {
|
|
20
|
+
...(error['extensions'] || {}),
|
|
21
|
+
stack: error['stack'],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
22
24
|
}
|
|
23
|
-
if (isDirectusError(
|
|
24
|
-
logger.debug(
|
|
25
|
+
if (isDirectusError(error)) {
|
|
26
|
+
logger.debug(error);
|
|
25
27
|
if (!status) {
|
|
26
|
-
status =
|
|
28
|
+
status = error.status;
|
|
27
29
|
}
|
|
28
|
-
else if (status !==
|
|
30
|
+
else if (status !== error.status) {
|
|
29
31
|
status = 500;
|
|
30
32
|
}
|
|
31
33
|
payload.errors.push({
|
|
32
|
-
message:
|
|
34
|
+
message: error.message,
|
|
33
35
|
extensions: {
|
|
34
|
-
code:
|
|
35
|
-
...(
|
|
36
|
+
code: error.code,
|
|
37
|
+
...(error.extensions ?? {}),
|
|
36
38
|
},
|
|
37
39
|
});
|
|
38
|
-
if (isDirectusError(
|
|
39
|
-
res.header('Allow',
|
|
40
|
+
if (isDirectusError(error, ErrorCode.MethodNotAllowed)) {
|
|
41
|
+
res.header('Allow', error.extensions.allowed.join(', '));
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
else {
|
|
43
|
-
logger.error(
|
|
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:
|
|
53
|
+
message: message || 'An unexpected error occurred.',
|
|
50
54
|
extensions: {
|
|
51
55
|
code: 'INTERNAL_SERVER_ERROR',
|
|
52
|
-
...
|
|
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
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* - 'access_token' query parameter
|
|
6
|
+
* - 'Authorization' header
|
|
7
|
+
* - Session cookie
|
|
6
8
|
*
|
|
7
|
-
* and store
|
|
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;
|