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