@directus/api 18.1.0 → 18.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.js +1 -1
- package/dist/extensions/lib/sandbox/register/operation.d.ts +1 -1
- package/dist/extensions/lib/sandbox/register/operation.js +4 -1
- package/dist/extensions/lib/sandbox/sdk/generators/request.js +2 -2
- package/dist/extensions/lib/sandbox/sdk/instantiate.js +1 -1
- package/dist/extensions/lib/sandbox/sdk/utils/wrap.d.ts +4 -2
- package/dist/extensions/lib/sandbox/sdk/utils/wrap.js +38 -4
- package/dist/extensions/lib/sync-extensions.d.ts +3 -1
- package/dist/extensions/lib/sync-extensions.js +45 -37
- package/dist/extensions/manager.d.ts +3 -1
- package/dist/extensions/manager.js +26 -24
- package/dist/flows.js +3 -3
- package/dist/lock/index.d.ts +1 -0
- package/dist/lock/index.js +1 -0
- package/dist/lock/lib/use-lock.d.ts +8 -0
- package/dist/lock/lib/use-lock.js +20 -0
- package/dist/operations/mail/index.js +5 -1
- package/dist/redis/utils/redis-config-available.js +3 -0
- package/dist/services/graphql/index.js +14 -5
- package/dist/services/graphql/schema-cache.d.ts +3 -0
- package/dist/services/graphql/schema-cache.js +12 -0
- package/dist/services/mail/index.d.ts +1 -1
- package/dist/services/mail/index.js +2 -4
- package/dist/services/notifications.js +12 -13
- package/dist/services/shares.js +7 -1
- package/dist/services/users.js +12 -2
- package/package.json +16 -15
- package/dist/utils/sanitize-error.d.ts +0 -1
- package/dist/utils/sanitize-error.js +0 -7
|
@@ -66,7 +66,7 @@ export function generateApiExtensionsSandboxEntrypoint(type, name, endpointRoute
|
|
|
66
66
|
|
|
67
67
|
const registerOperation = ${generateHostFunctionReference(index, ['id', 'handler'], { async: false })}
|
|
68
68
|
|
|
69
|
-
const operationConfig = extensionExport
|
|
69
|
+
const operationConfig = extensionExport;
|
|
70
70
|
|
|
71
71
|
registerOperation(operationConfig.id, operationConfig.handler);
|
|
72
72
|
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PromiseCallback } from '@directus/types';
|
|
2
2
|
import type { Reference } from 'isolated-vm';
|
|
3
3
|
export declare function registerOperationGenerator(): {
|
|
4
|
-
register: (id: Reference<string>, cb: Reference<(
|
|
4
|
+
register: (id: Reference<string>, cb: Reference<(options: Record<string, unknown>) => unknown | Promise<unknown> | void>) => void;
|
|
5
5
|
unregisterFunctions: PromiseCallback[];
|
|
6
6
|
};
|
|
@@ -9,7 +9,10 @@ export function registerOperationGenerator() {
|
|
|
9
9
|
if (cb.typeof !== 'function')
|
|
10
10
|
throw new TypeError('Operation config handler has to be of type function');
|
|
11
11
|
const idCopied = id.copySync();
|
|
12
|
-
const handler = async (
|
|
12
|
+
const handler = async (options) => {
|
|
13
|
+
const response = await callReference(cb, [options]);
|
|
14
|
+
return response.copy();
|
|
15
|
+
};
|
|
13
16
|
flowManager.addOperation(idCopied, handler);
|
|
14
17
|
unregisterFunctions.push(() => {
|
|
15
18
|
flowManager.removeOperation(idCopied);
|
|
@@ -28,8 +28,8 @@ export function requestGenerator(requestedScopes) {
|
|
|
28
28
|
if (body !== undefined && body.typeof !== 'undefined' && body.typeof !== 'string' && body.typeof !== 'object') {
|
|
29
29
|
throw new TypeError('Request body has to be of type string or object');
|
|
30
30
|
}
|
|
31
|
-
if (headers !== undefined && headers.typeof !== 'undefined' && headers.typeof !== '
|
|
32
|
-
throw new TypeError('Request headers has to be of type
|
|
31
|
+
if (headers !== undefined && headers.typeof !== 'undefined' && headers.typeof !== 'object') {
|
|
32
|
+
throw new TypeError('Request headers has to be of type object');
|
|
33
33
|
}
|
|
34
34
|
const methodCopied = await method?.copy();
|
|
35
35
|
const bodyCopied = await body?.copy();
|
|
@@ -18,7 +18,7 @@ export async function instantiateSandboxSdk(isolate, requestedScopes) {
|
|
|
18
18
|
const handlerCode = sdk
|
|
19
19
|
.map(({ name, args, async }) => `sdk.${name} = ${generateHostFunctionReference(index, args, { async })}`)
|
|
20
20
|
.join('\n');
|
|
21
|
-
await apiContext.evalClosure(handlerCode, sdk.map(({ generator, async }) =>
|
|
21
|
+
await apiContext.evalClosure(handlerCode, sdk.map(({ name, generator, async }) => async ? wrap(name, generator(requestedScopes)) : generator(requestedScopes)), { filename: '<extensions-sdk>', arguments: { reference: true } });
|
|
22
22
|
const exportCode = sdk.map(({ name }) => `export const ${name} = sdk.${name};`).join('\n');
|
|
23
23
|
const apiModule = await isolate.compileModule(exportCode);
|
|
24
24
|
await apiModule.instantiate(apiContext, () => {
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is needed as isolated-vm doesn't allow the isolate to catch errors that are thrown in the
|
|
5
5
|
* host. Instead, we'll wrap the output in a known shape which allows the isolated sdk context to
|
|
6
|
-
* re-throw the error in the correct context
|
|
6
|
+
* re-throw the error in the correct context.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/laverdet/isolated-vm/issues/417
|
|
7
9
|
*/
|
|
8
|
-
export declare function wrap(util: (...args: any[]) => any): (...args: any[]) => Promise<{
|
|
10
|
+
export declare function wrap(name: string, util: (...args: any[]) => any): (...args: any[]) => Promise<{
|
|
9
11
|
result: any;
|
|
10
12
|
error: boolean;
|
|
11
13
|
}>;
|
|
@@ -3,15 +3,49 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is needed as isolated-vm doesn't allow the isolate to catch errors that are thrown in the
|
|
5
5
|
* host. Instead, we'll wrap the output in a known shape which allows the isolated sdk context to
|
|
6
|
-
* re-throw the error in the correct context
|
|
6
|
+
* re-throw the error in the correct context.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/laverdet/isolated-vm/issues/417
|
|
7
9
|
*/
|
|
8
|
-
export function wrap(util) {
|
|
10
|
+
export function wrap(name, util) {
|
|
9
11
|
return async (...args) => {
|
|
10
12
|
try {
|
|
11
13
|
return { result: await util(...args), error: false };
|
|
12
14
|
}
|
|
13
|
-
catch (
|
|
14
|
-
|
|
15
|
+
catch (error) {
|
|
16
|
+
// isolated-vm expects objects thrown from within the vm to be an instance of `Error`
|
|
17
|
+
let result;
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
// Don't expose the stack trace to the vm
|
|
20
|
+
delete error.stack;
|
|
21
|
+
// Serialize the remaining error properties
|
|
22
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
23
|
+
const value = error[key];
|
|
24
|
+
if (!value || typeof value !== 'object')
|
|
25
|
+
continue;
|
|
26
|
+
error[key] = JSON.stringify(value, getCircularReplacer());
|
|
27
|
+
}
|
|
28
|
+
result = error;
|
|
29
|
+
}
|
|
30
|
+
else if (error && typeof error !== 'object') {
|
|
31
|
+
result = error;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
result = new Error(`Unknown error in "${name}" Sandbox SDK function`);
|
|
35
|
+
}
|
|
36
|
+
return { result, error: true };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function getCircularReplacer() {
|
|
41
|
+
const seen = new WeakSet();
|
|
42
|
+
return (_key, value) => {
|
|
43
|
+
if (value !== null && typeof value === 'object') {
|
|
44
|
+
if (seen.has(value)) {
|
|
45
|
+
return '[Circular]';
|
|
46
|
+
}
|
|
47
|
+
seen.add(value);
|
|
15
48
|
}
|
|
49
|
+
return value;
|
|
16
50
|
};
|
|
17
51
|
}
|
|
@@ -7,56 +7,64 @@ import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
8
|
import Queue from 'p-queue';
|
|
9
9
|
import { useBus } from '../../bus/index.js';
|
|
10
|
+
import { useLock } from '../../lock/index.js';
|
|
10
11
|
import { useLogger } from '../../logger.js';
|
|
11
12
|
import { getStorage } from '../../storage/index.js';
|
|
12
13
|
import { getExtensionsPath } from './get-extensions-path.js';
|
|
13
14
|
import { SyncStatus, getSyncStatus, setSyncStatus } from './sync-status.js';
|
|
14
|
-
export const syncExtensions = async () => {
|
|
15
|
+
export const syncExtensions = async (options) => {
|
|
16
|
+
const lock = useLock();
|
|
17
|
+
const messenger = useBus();
|
|
15
18
|
const env = useEnv();
|
|
16
19
|
const logger = useLogger();
|
|
17
|
-
|
|
18
|
-
const storageExtensionsPath = env['EXTENSIONS_PATH'];
|
|
19
|
-
const messenger = useBus();
|
|
20
|
-
const isPrimaryProcess = String(process.env['NODE_APP_INSTANCE']) === '0' || process.env['NODE_APP_INSTANCE'] === undefined;
|
|
21
|
-
const id = await mid.machineId();
|
|
22
|
-
const message = `extensions-sync/${id}`;
|
|
23
|
-
if (isPrimaryProcess === false) {
|
|
20
|
+
if (!options?.force) {
|
|
24
21
|
const isDone = (await getSyncStatus()) === SyncStatus.DONE;
|
|
25
22
|
if (isDone)
|
|
26
23
|
return;
|
|
24
|
+
}
|
|
25
|
+
const machineId = await mid.machineId();
|
|
26
|
+
const machineKey = `extensions-sync/${machineId}`;
|
|
27
|
+
const processId = await lock.increment(machineKey);
|
|
28
|
+
const currentProcessShouldHandleSync = processId === 1;
|
|
29
|
+
if (currentProcessShouldHandleSync === false) {
|
|
27
30
|
logger.trace('Extensions already being synced to this machine from another process.');
|
|
28
|
-
|
|
29
|
-
* Wait until the process that called the lock publishes a message that the syncing is complete
|
|
30
|
-
*/
|
|
31
|
+
// Wait until the process that called the lock publishes a message that the syncing is complete
|
|
31
32
|
return new Promise((resolve) => {
|
|
32
|
-
messenger.subscribe(
|
|
33
|
+
messenger.subscribe(machineKey, () => resolve());
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
try {
|
|
37
|
+
const extensionsPath = getExtensionsPath();
|
|
38
|
+
const storageExtensionsPath = env['EXTENSIONS_PATH'];
|
|
39
|
+
if (await exists(extensionsPath)) {
|
|
40
|
+
// In case the FS still contains the cached extensions from a previous invocation. We have to
|
|
41
|
+
// clear them out to ensure the remote extensions folder remains the source of truth for all
|
|
42
|
+
// extensions that are loaded.
|
|
43
|
+
await rm(extensionsPath, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
// Ensure that the local extensions cache path exists
|
|
46
|
+
await mkdir(extensionsPath, { recursive: true });
|
|
47
|
+
await setSyncStatus(SyncStatus.SYNCING);
|
|
48
|
+
logger.trace('Syncing extensions from configured storage location...');
|
|
49
|
+
const storage = await getStorage();
|
|
50
|
+
const disk = storage.location(env['EXTENSIONS_LOCATION']);
|
|
51
|
+
// Make sure we don't overload the file handles
|
|
52
|
+
const queue = new Queue({ concurrency: 1000 });
|
|
53
|
+
for await (const filepath of disk.list(storageExtensionsPath)) {
|
|
54
|
+
const readStream = await disk.read(filepath);
|
|
55
|
+
// We want files to be stored in the root of `$TEMP_PATH/extensions`, so gotta remove the
|
|
56
|
+
// extensions path on disk from the start of the file path
|
|
57
|
+
const destPath = join(extensionsPath, relative(resolve(sep, storageExtensionsPath), resolve(sep, filepath)));
|
|
58
|
+
// Ensure that the directory path exists
|
|
59
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
60
|
+
const writeStream = createWriteStream(destPath);
|
|
61
|
+
queue.add(() => pipeline(readStream, writeStream));
|
|
62
|
+
}
|
|
63
|
+
await queue.onIdle();
|
|
64
|
+
await setSyncStatus(SyncStatus.DONE);
|
|
65
|
+
messenger.publish(machineKey, { ready: true });
|
|
40
66
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await setSyncStatus(SyncStatus.SYNCING);
|
|
44
|
-
logger.trace('Syncing extensions from configured storage location...');
|
|
45
|
-
const storage = await getStorage();
|
|
46
|
-
const disk = storage.location(env['EXTENSIONS_LOCATION']);
|
|
47
|
-
// Make sure we don't overload the file handles
|
|
48
|
-
const queue = new Queue({ concurrency: 1000 });
|
|
49
|
-
for await (const filepath of disk.list(storageExtensionsPath)) {
|
|
50
|
-
const readStream = await disk.read(filepath);
|
|
51
|
-
// We want files to be stored in the root of `$TEMP_PATH/extensions`, so gotta remove the
|
|
52
|
-
// extensions path on disk from the start of the file path
|
|
53
|
-
const destPath = join(extensionsPath, relative(resolve(sep, storageExtensionsPath), resolve(sep, filepath)));
|
|
54
|
-
// Ensure that the directory path exists
|
|
55
|
-
await mkdir(dirname(destPath), { recursive: true });
|
|
56
|
-
const writeStream = createWriteStream(destPath);
|
|
57
|
-
queue.add(() => pipeline(readStream, writeStream));
|
|
67
|
+
finally {
|
|
68
|
+
await lock.delete(machineKey);
|
|
58
69
|
}
|
|
59
|
-
await queue.onIdle();
|
|
60
|
-
await setSyncStatus(SyncStatus.DONE);
|
|
61
|
-
messenger.publish(message, { ready: true });
|
|
62
70
|
};
|
|
@@ -92,7 +92,9 @@ export declare class ExtensionManager {
|
|
|
92
92
|
/**
|
|
93
93
|
* Reload all the extensions. Will unload if extensions have already been loaded
|
|
94
94
|
*/
|
|
95
|
-
reload(
|
|
95
|
+
reload(options?: {
|
|
96
|
+
forceSync: boolean;
|
|
97
|
+
}): Promise<unknown>;
|
|
96
98
|
/**
|
|
97
99
|
* Return the previously generated app extensions bundle
|
|
98
100
|
*/
|
|
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES } from '@directus/extensions';
|
|
3
3
|
import { generateExtensionsEntrypoint } from '@directus/extensions/node';
|
|
4
4
|
import { isTypeIn, toBoolean } from '@directus/utils';
|
|
5
|
-
import {
|
|
5
|
+
import { pathToRelativeUrl, processId } from '@directus/utils/node';
|
|
6
6
|
import aliasDefault from '@rollup/plugin-alias';
|
|
7
7
|
import nodeResolveDefault from '@rollup/plugin-node-resolve';
|
|
8
8
|
import virtualDefault from '@rollup/plugin-virtual';
|
|
@@ -45,7 +45,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
45
45
|
const env = useEnv();
|
|
46
46
|
const defaultOptions = {
|
|
47
47
|
schedule: true,
|
|
48
|
-
watch: env['EXTENSIONS_AUTO_RELOAD']
|
|
48
|
+
watch: env['EXTENSIONS_AUTO_RELOAD'],
|
|
49
49
|
};
|
|
50
50
|
export class ExtensionManager {
|
|
51
51
|
options = defaultOptions;
|
|
@@ -170,22 +170,22 @@ export class ExtensionManager {
|
|
|
170
170
|
*/
|
|
171
171
|
async install(versionId) {
|
|
172
172
|
await this.installationManager.install(versionId);
|
|
173
|
-
await this.reload();
|
|
173
|
+
await this.reload({ forceSync: true });
|
|
174
174
|
await this.messenger.publish(this.reloadChannel, { origin: this.processId });
|
|
175
175
|
}
|
|
176
176
|
async uninstall(folder) {
|
|
177
177
|
await this.installationManager.uninstall(folder);
|
|
178
|
-
await this.reload();
|
|
178
|
+
await this.reload({ forceSync: true });
|
|
179
179
|
await this.messenger.publish(this.reloadChannel, { origin: this.processId });
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
182
|
* Load all extensions from disk and register them in their respective places
|
|
183
183
|
*/
|
|
184
|
-
async load() {
|
|
184
|
+
async load(options) {
|
|
185
185
|
const logger = useLogger();
|
|
186
186
|
if (env['EXTENSIONS_LOCATION']) {
|
|
187
187
|
try {
|
|
188
|
-
await syncExtensions();
|
|
188
|
+
await syncExtensions({ force: options?.forceSync ?? false });
|
|
189
189
|
}
|
|
190
190
|
catch (error) {
|
|
191
191
|
logger.error(`Failed to sync extensions`);
|
|
@@ -221,7 +221,7 @@ export class ExtensionManager {
|
|
|
221
221
|
/**
|
|
222
222
|
* Reload all the extensions. Will unload if extensions have already been loaded
|
|
223
223
|
*/
|
|
224
|
-
reload() {
|
|
224
|
+
reload(options) {
|
|
225
225
|
if (this.reloadQueue.size > 0) {
|
|
226
226
|
// The pending job in the queue will already handle the additional changes
|
|
227
227
|
return Promise.resolve();
|
|
@@ -237,7 +237,7 @@ export class ExtensionManager {
|
|
|
237
237
|
if (this.isLoaded) {
|
|
238
238
|
const prevExtensions = clone(this.extensions);
|
|
239
239
|
await this.unload();
|
|
240
|
-
await this.load();
|
|
240
|
+
await this.load(options);
|
|
241
241
|
logger.info('Extensions reloaded');
|
|
242
242
|
const added = this.extensions.filter((extension) => !prevExtensions.some((prevExtension) => extension.path === prevExtension.path));
|
|
243
243
|
const removed = prevExtensions.filter((prevExtension) => !this.extensions.some((extension) => prevExtension.path === extension.path));
|
|
@@ -294,7 +294,8 @@ export class ExtensionManager {
|
|
|
294
294
|
logger.info('Watching extensions for changes...');
|
|
295
295
|
const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
|
|
296
296
|
this.watcher = chokidar.watch([path.resolve('package.json'), path.posix.join(extensionDirUrl, '*', 'package.json')], {
|
|
297
|
-
ignoreInitial: true,
|
|
297
|
+
ignoreInitial: true,
|
|
298
|
+
// dotdirs are watched by default and frequently found in 'node_modules'
|
|
298
299
|
ignored: `${extensionDirUrl}/**/node_modules/**`,
|
|
299
300
|
// on macOS dotdirs in linked extensions are watched too
|
|
300
301
|
followSymlinks: os.platform() === 'darwin' ? false : true,
|
|
@@ -318,20 +319,20 @@ export class ExtensionManager {
|
|
|
318
319
|
* removed
|
|
319
320
|
*/
|
|
320
321
|
updateWatchedExtensions(added, removed = []) {
|
|
321
|
-
if (this.watcher)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
322
|
+
if (!this.watcher)
|
|
323
|
+
return;
|
|
324
|
+
const extensionDir = path.resolve(getExtensionsPath());
|
|
325
|
+
const registryDir = path.join(extensionDir, '.registry');
|
|
326
|
+
const toPackageExtensionPaths = (extensions) => extensions
|
|
327
|
+
.filter((extension) => extension.local && extension.path.startsWith(extensionDir) && !extension.path.startsWith(registryDir))
|
|
328
|
+
.flatMap((extension) => isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
|
|
329
|
+
? [
|
|
330
|
+
path.resolve(extension.path, extension.entrypoint.app),
|
|
331
|
+
path.resolve(extension.path, extension.entrypoint.api),
|
|
332
|
+
]
|
|
333
|
+
: path.resolve(extension.path, extension.entrypoint));
|
|
334
|
+
this.watcher.add(toPackageExtensionPaths(added));
|
|
335
|
+
this.watcher.unwatch(toPackageExtensionPaths(removed));
|
|
335
336
|
}
|
|
336
337
|
/**
|
|
337
338
|
* Uses rollup to bundle the app extensions together into a single file the app can download and
|
|
@@ -399,7 +400,8 @@ export class ExtensionManager {
|
|
|
399
400
|
});
|
|
400
401
|
this.unregisterFunctionMap.set(extension.name, async () => {
|
|
401
402
|
await unregisterFunction();
|
|
402
|
-
isolate.
|
|
403
|
+
if (!isolate.isDisposed)
|
|
404
|
+
isolate.dispose();
|
|
403
405
|
});
|
|
404
406
|
}
|
|
405
407
|
async registerApiExtensions() {
|
package/dist/flows.js
CHANGED
|
@@ -17,7 +17,6 @@ import { getSchema } from './utils/get-schema.js';
|
|
|
17
17
|
import { JobQueue } from './utils/job-queue.js';
|
|
18
18
|
import { mapValuesDeep } from './utils/map-values-deep.js';
|
|
19
19
|
import { redactObject } from './utils/redact-object.js';
|
|
20
|
-
import { sanitizeError } from './utils/sanitize-error.js';
|
|
21
20
|
import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
|
|
22
21
|
import { isSystemCollection } from '@directus/system-data';
|
|
23
22
|
let flowManager;
|
|
@@ -341,8 +340,9 @@ class FlowManager {
|
|
|
341
340
|
catch (error) {
|
|
342
341
|
let data;
|
|
343
342
|
if (error instanceof Error) {
|
|
344
|
-
//
|
|
345
|
-
|
|
343
|
+
// Don't expose the stack trace to the next operation
|
|
344
|
+
delete error.stack;
|
|
345
|
+
data = error;
|
|
346
346
|
}
|
|
347
347
|
else if (typeof error === 'string') {
|
|
348
348
|
// If the error is a JSON string, parse it and use that as the error data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/use-lock.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/use-lock.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createKv } from '@directus/memory';
|
|
2
|
+
import { redisConfigAvailable, useRedis } from '../../redis/index.js';
|
|
3
|
+
export const _cache = {
|
|
4
|
+
lock: undefined,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Returns globally shared lock kv instance.
|
|
8
|
+
*/
|
|
9
|
+
export const useLock = () => {
|
|
10
|
+
if (_cache.lock) {
|
|
11
|
+
return _cache.lock;
|
|
12
|
+
}
|
|
13
|
+
if (redisConfigAvailable()) {
|
|
14
|
+
_cache.lock = createKv({ type: 'redis', redis: useRedis(), namespace: 'directus:lock' });
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
_cache.lock = createKv({ type: 'local' });
|
|
18
|
+
}
|
|
19
|
+
return _cache.lock;
|
|
20
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineOperationApi } from '@directus/extensions';
|
|
2
2
|
import { MailService } from '../../services/mail/index.js';
|
|
3
3
|
import { md } from '../../utils/md.js';
|
|
4
|
+
import { useLogger } from '../../logger.js';
|
|
5
|
+
const logger = useLogger();
|
|
4
6
|
export default defineOperationApi({
|
|
5
7
|
id: 'mail',
|
|
6
8
|
handler: async ({ body, template, data, to, type, subject }, { accountability, database, getSchema }) => {
|
|
@@ -16,6 +18,8 @@ export default defineOperationApi({
|
|
|
16
18
|
else {
|
|
17
19
|
mailObject.html = type === 'wysiwyg' ? safeBody : md(safeBody);
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
mailService.send(mailObject).catch((error) => {
|
|
22
|
+
logger.error(error, 'Could not send mail in "mail" operation');
|
|
23
|
+
});
|
|
20
24
|
},
|
|
21
25
|
});
|
|
@@ -4,5 +4,8 @@ import { useEnv } from '@directus/env';
|
|
|
4
4
|
*/
|
|
5
5
|
export const redisConfigAvailable = () => {
|
|
6
6
|
const env = useEnv();
|
|
7
|
+
if ('REDIS_ENABLED' in env) {
|
|
8
|
+
return env['REDIS_ENABLED'] === true;
|
|
9
|
+
}
|
|
7
10
|
return 'REDIS' in env || Object.keys(env).some((key) => key.startsWith('REDIS_'));
|
|
8
11
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Action, FUNCTIONS } from '@directus/constants';
|
|
2
2
|
import { useEnv } from '@directus/env';
|
|
3
3
|
import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
|
|
4
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
4
5
|
import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
|
|
5
6
|
import argon2 from 'argon2';
|
|
6
7
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
|
|
@@ -12,6 +13,8 @@ import getDatabase from '../../database/index.js';
|
|
|
12
13
|
import { generateHash } from '../../utils/generate-hash.js';
|
|
13
14
|
import { getGraphQLType } from '../../utils/get-graphql-type.js';
|
|
14
15
|
import { getService } from '../../utils/get-service.js';
|
|
16
|
+
import isDirectusJWT from '../../utils/is-directus-jwt.js';
|
|
17
|
+
import { verifyAccessJWT } from '../../utils/jwt.js';
|
|
15
18
|
import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-version-data.js';
|
|
16
19
|
import { reduceSchema } from '../../utils/reduce-schema.js';
|
|
17
20
|
import { sanitizeQuery } from '../../utils/sanitize-query.js';
|
|
@@ -31,6 +34,7 @@ import { UsersService } from '../users.js';
|
|
|
31
34
|
import { UtilsService } from '../utils.js';
|
|
32
35
|
import { VersionsService } from '../versions.js';
|
|
33
36
|
import { GraphQLExecutionError, GraphQLValidationError } from './errors/index.js';
|
|
37
|
+
import { cache } from './schema-cache.js';
|
|
34
38
|
import { createSubscriptionGenerator } from './subscription.js';
|
|
35
39
|
import { GraphQLBigInt } from './types/bigint.js';
|
|
36
40
|
import { GraphQLDate } from './types/date.js';
|
|
@@ -40,9 +44,6 @@ import { GraphQLStringOrFloat } from './types/string-or-float.js';
|
|
|
40
44
|
import { GraphQLVoid } from './types/void.js';
|
|
41
45
|
import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
|
|
42
46
|
import processError from './utils/process-error.js';
|
|
43
|
-
import { isSystemCollection } from '@directus/system-data';
|
|
44
|
-
import isDirectusJWT from '../../utils/is-directus-jwt.js';
|
|
45
|
-
import { verifyAccessJWT } from '../../utils/jwt.js';
|
|
46
47
|
const env = useEnv();
|
|
47
48
|
const validationRules = Array.from(specifiedRules);
|
|
48
49
|
if (env['GRAPHQL_INTROSPECTION'] === false) {
|
|
@@ -104,6 +105,10 @@ export class GraphQLService {
|
|
|
104
105
|
return formattedResult;
|
|
105
106
|
}
|
|
106
107
|
getSchema(type = 'schema') {
|
|
108
|
+
const key = `${type}_${this.accountability?.role}_${this.accountability?.user}`;
|
|
109
|
+
const cachedSchema = cache.get(key);
|
|
110
|
+
if (cachedSchema)
|
|
111
|
+
return cachedSchema;
|
|
107
112
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
108
113
|
const self = this;
|
|
109
114
|
const schemaComposer = new SchemaComposer();
|
|
@@ -218,9 +223,13 @@ export class GraphQLService {
|
|
|
218
223
|
}, {}));
|
|
219
224
|
}
|
|
220
225
|
if (type === 'sdl') {
|
|
221
|
-
|
|
226
|
+
const sdl = schemaComposer.toSDL();
|
|
227
|
+
cache.set(key, sdl);
|
|
228
|
+
return sdl;
|
|
222
229
|
}
|
|
223
|
-
|
|
230
|
+
const gqlSchema = schemaComposer.buildSchema();
|
|
231
|
+
cache.set(key, gqlSchema);
|
|
232
|
+
return gqlSchema;
|
|
224
233
|
/**
|
|
225
234
|
* Construct an object of types for every collection, using the permitted fields per action type
|
|
226
235
|
* as it's fields.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { GraphQLSchema } from 'graphql';
|
|
3
|
+
import LRUMapDefault from 'mnemonist/lru-map.js';
|
|
4
|
+
import { useBus } from '../../bus/index.js';
|
|
5
|
+
// Workaround for misaligned types in mnemonist package exports
|
|
6
|
+
const LRUMap = LRUMapDefault;
|
|
7
|
+
const env = useEnv();
|
|
8
|
+
const bus = useBus();
|
|
9
|
+
export const cache = new LRUMap(Number(env['GRAPHQL_SCHEMA_CACHE_CAPACITY'] ?? 100));
|
|
10
|
+
bus.subscribe('schemaChanged', () => {
|
|
11
|
+
cache.clear();
|
|
12
|
+
});
|
|
@@ -14,7 +14,7 @@ export declare class MailService {
|
|
|
14
14
|
knex: Knex;
|
|
15
15
|
mailer: Transporter;
|
|
16
16
|
constructor(opts: AbstractServiceOptions);
|
|
17
|
-
send(options: EmailOptions): Promise<
|
|
17
|
+
send<T>(options: EmailOptions): Promise<T>;
|
|
18
18
|
private renderTemplate;
|
|
19
19
|
private getDefaultTemplateData;
|
|
20
20
|
}
|
|
@@ -54,10 +54,8 @@ export class MailService {
|
|
|
54
54
|
.map((line) => line.trim())
|
|
55
55
|
.join('\n');
|
|
56
56
|
}
|
|
57
|
-
this.mailer.sendMail({ ...emailOptions, from, html })
|
|
58
|
-
|
|
59
|
-
logger.warn(error);
|
|
60
|
-
});
|
|
57
|
+
const info = await this.mailer.sendMail({ ...emailOptions, from, html });
|
|
58
|
+
return info;
|
|
61
59
|
}
|
|
62
60
|
async renderTemplate(template, variables) {
|
|
63
61
|
const customTemplatePath = path.resolve(env['EMAIL_TEMPLATES_PATH'], template + '.liquid');
|
|
@@ -37,19 +37,18 @@ export class NotificationsService extends ItemsService {
|
|
|
37
37
|
.toString();
|
|
38
38
|
const html = data.message ? md(data.message) : '';
|
|
39
39
|
if (user['email'] && user['email_notifications'] === true) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
40
|
+
this.mailService
|
|
41
|
+
.send({
|
|
42
|
+
template: {
|
|
43
|
+
name: 'base',
|
|
44
|
+
data: user['role']?.app_access ? { url: manageUserAccountUrl, html } : { html },
|
|
45
|
+
},
|
|
46
|
+
to: user['email'],
|
|
47
|
+
subject: data.subject,
|
|
48
|
+
})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
logger.error(error, `Could not send notification via mail`);
|
|
51
|
+
});
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
}
|
package/dist/services/shares.js
CHANGED
|
@@ -10,7 +10,9 @@ import { AuthorizationService } from './authorization.js';
|
|
|
10
10
|
import { ItemsService } from './items.js';
|
|
11
11
|
import { MailService } from './mail/index.js';
|
|
12
12
|
import { UsersService } from './users.js';
|
|
13
|
+
import { useLogger } from '../logger.js';
|
|
13
14
|
const env = useEnv();
|
|
15
|
+
const logger = useLogger();
|
|
14
16
|
export class SharesService extends ItemsService {
|
|
15
17
|
authorizationService;
|
|
16
18
|
constructor(options) {
|
|
@@ -119,7 +121,8 @@ ${userName(userInfo)} has invited you to view an item in ${share['collection']}.
|
|
|
119
121
|
[Open](${new Url(env['PUBLIC_URL']).addPath('admin', 'shared', payload.share).toString()})
|
|
120
122
|
`;
|
|
121
123
|
for (const email of payload.emails) {
|
|
122
|
-
|
|
124
|
+
mailService
|
|
125
|
+
.send({
|
|
123
126
|
template: {
|
|
124
127
|
name: 'base',
|
|
125
128
|
data: {
|
|
@@ -128,6 +131,9 @@ ${userName(userInfo)} has invited you to view an item in ${share['collection']}.
|
|
|
128
131
|
},
|
|
129
132
|
to: email,
|
|
130
133
|
subject: `${userName(userInfo)} has shared an item with you`,
|
|
134
|
+
})
|
|
135
|
+
.catch((error) => {
|
|
136
|
+
logger.error(error, `Could not send share notification mail`);
|
|
131
137
|
});
|
|
132
138
|
}
|
|
133
139
|
}
|
package/dist/services/users.js
CHANGED
|
@@ -14,7 +14,9 @@ import { Url } from '../utils/url.js';
|
|
|
14
14
|
import { ItemsService } from './items.js';
|
|
15
15
|
import { MailService } from './mail/index.js';
|
|
16
16
|
import { SettingsService } from './settings.js';
|
|
17
|
+
import { useLogger } from '../logger.js';
|
|
17
18
|
const env = useEnv();
|
|
19
|
+
const logger = useLogger();
|
|
18
20
|
export class UsersService extends ItemsService {
|
|
19
21
|
constructor(options) {
|
|
20
22
|
super('directus_users', options);
|
|
@@ -335,7 +337,8 @@ export class UsersService extends ItemsService {
|
|
|
335
337
|
// Send invite for new and already invited users
|
|
336
338
|
if (isEmpty(user) || user.status === 'invited') {
|
|
337
339
|
const subjectLine = subject ?? "You've been invited";
|
|
338
|
-
|
|
340
|
+
mailService
|
|
341
|
+
.send({
|
|
339
342
|
to: user?.email ?? email,
|
|
340
343
|
subject: subjectLine,
|
|
341
344
|
template: {
|
|
@@ -345,6 +348,9 @@ export class UsersService extends ItemsService {
|
|
|
345
348
|
email: user?.email ?? email,
|
|
346
349
|
},
|
|
347
350
|
},
|
|
351
|
+
})
|
|
352
|
+
.catch((error) => {
|
|
353
|
+
logger.error(error, `Could not send user invitation mail`);
|
|
348
354
|
});
|
|
349
355
|
}
|
|
350
356
|
}
|
|
@@ -386,7 +392,8 @@ export class UsersService extends ItemsService {
|
|
|
386
392
|
? new Url(url).setQuery('token', token).toString()
|
|
387
393
|
: new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
|
|
388
394
|
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
389
|
-
|
|
395
|
+
mailService
|
|
396
|
+
.send({
|
|
390
397
|
to: user.email,
|
|
391
398
|
subject: subjectLine,
|
|
392
399
|
template: {
|
|
@@ -396,6 +403,9 @@ export class UsersService extends ItemsService {
|
|
|
396
403
|
email: user.email,
|
|
397
404
|
},
|
|
398
405
|
},
|
|
406
|
+
})
|
|
407
|
+
.catch((error) => {
|
|
408
|
+
logger.error(error, `Could not send password reset mail`);
|
|
399
409
|
});
|
|
400
410
|
await stall(STALL_TIME, timeStart);
|
|
401
411
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.2.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
|
62
|
-
"@aws-sdk/client-ses": "3.
|
|
62
|
+
"@aws-sdk/client-ses": "3.529.0",
|
|
63
63
|
"@directus/format-title": "10.1.0",
|
|
64
64
|
"@godaddy/terminus": "4.12.1",
|
|
65
65
|
"@rollup/plugin-alias": "5.1.0",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"micromustache": "8.0.3",
|
|
114
114
|
"mime-types": "2.1.35",
|
|
115
115
|
"minimatch": "9.0.3",
|
|
116
|
+
"mnemonist": "0.39.8",
|
|
116
117
|
"ms": "2.1.3",
|
|
117
118
|
"nanoid": "5.0.6",
|
|
118
119
|
"node-machine-id": "1.1.12",
|
|
@@ -120,7 +121,7 @@
|
|
|
120
121
|
"nodemailer": "6.9.11",
|
|
121
122
|
"object-hash": "3.0.0",
|
|
122
123
|
"openapi3-ts": "4.2.2",
|
|
123
|
-
"openid-client": "5.6.
|
|
124
|
+
"openid-client": "5.6.5",
|
|
124
125
|
"ora": "8.0.1",
|
|
125
126
|
"otplib": "12.0.1",
|
|
126
127
|
"p-limit": "5.0.0",
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
"pino-http": "9.0.0",
|
|
131
132
|
"pino-http-print": "3.1.0",
|
|
132
133
|
"pino-pretty": "10.3.1",
|
|
133
|
-
"qs": "6.
|
|
134
|
+
"qs": "6.12.0",
|
|
134
135
|
"rate-limiter-flexible": "5.0.0",
|
|
135
136
|
"rollup": "4.12.0",
|
|
136
137
|
"samlify": "2.8.11",
|
|
@@ -144,28 +145,28 @@
|
|
|
144
145
|
"ws": "8.16.0",
|
|
145
146
|
"zod": "3.22.4",
|
|
146
147
|
"zod-validation-error": "3.0.3",
|
|
147
|
-
"@directus/app": "11.0.
|
|
148
|
-
"@directus/
|
|
149
|
-
"@directus/env": "1.0.4",
|
|
148
|
+
"@directus/app": "11.0.3",
|
|
149
|
+
"@directus/env": "1.1.0",
|
|
150
150
|
"@directus/constants": "11.0.3",
|
|
151
|
-
"@directus/
|
|
152
|
-
"@directus/memory": "1.0.4",
|
|
151
|
+
"@directus/errors": "0.2.4",
|
|
153
152
|
"@directus/extensions-registry": "1.0.1",
|
|
153
|
+
"@directus/extensions": "1.0.1",
|
|
154
154
|
"@directus/extensions-sdk": "11.0.1",
|
|
155
|
-
"@directus/
|
|
155
|
+
"@directus/memory": "1.0.5",
|
|
156
156
|
"@directus/pressure": "1.0.17",
|
|
157
|
-
"@directus/storage": "10.0.11",
|
|
158
157
|
"@directus/specs": "10.2.7",
|
|
158
|
+
"@directus/schema": "11.0.1",
|
|
159
|
+
"@directus/storage": "10.0.11",
|
|
159
160
|
"@directus/storage-driver-azure": "10.0.18",
|
|
160
161
|
"@directus/storage-driver-cloudinary": "10.0.18",
|
|
161
162
|
"@directus/storage-driver-gcs": "10.0.18",
|
|
162
163
|
"@directus/storage-driver-s3": "10.0.19",
|
|
163
164
|
"@directus/storage-driver-local": "10.0.18",
|
|
165
|
+
"@directus/storage-driver-supabase": "1.0.10",
|
|
164
166
|
"@directus/system-data": "1.0.1",
|
|
165
167
|
"@directus/utils": "11.0.6",
|
|
166
|
-
"
|
|
167
|
-
"@directus/validation": "0.0.13"
|
|
168
|
-
"directus": "10.10.1"
|
|
168
|
+
"directus": "10.10.3",
|
|
169
|
+
"@directus/validation": "0.0.13"
|
|
169
170
|
},
|
|
170
171
|
"devDependencies": {
|
|
171
172
|
"@ngneat/falso": "7.2.0",
|
|
@@ -227,7 +228,7 @@
|
|
|
227
228
|
"scripts": {
|
|
228
229
|
"build": "tsc --project tsconfig.prod.json && copyfiles \"src/**/*.{yaml,liquid}\" -u 1 dist",
|
|
229
230
|
"cli": "NODE_ENV=development SERVE_APP=false tsx src/cli/run.ts",
|
|
230
|
-
"dev": "NODE_ENV=development SERVE_APP=true tsx watch --clear-screen=false src/start.ts",
|
|
231
|
+
"dev": "NODE_ENV=development SERVE_APP=true tsx watch --ignore extensions --clear-screen=false src/start.ts",
|
|
231
232
|
"test": "vitest --watch=false"
|
|
232
233
|
}
|
|
233
234
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function sanitizeError<T extends Error>(error: T): T;
|