@directus/api 13.2.0 → 14.0.1
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/__utils__/snapshots.js +9 -0
- package/dist/app.js +2 -0
- package/dist/cli/index.js +3 -4
- package/dist/cli/load-extensions.d.ts +1 -0
- package/dist/cli/load-extensions.js +19 -0
- package/dist/controllers/extensions.js +28 -19
- package/dist/controllers/versions.d.ts +2 -0
- package/dist/controllers/versions.js +188 -0
- package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20230823A-add-content-versioning.js +36 -0
- package/dist/database/migrations/20230927A-themes.d.ts +3 -0
- package/dist/database/migrations/20230927A-themes.js +49 -0
- package/dist/database/migrations/20231009B-update-panel-options.d.ts +3 -0
- package/dist/database/migrations/20231009B-update-panel-options.js +77 -0
- package/dist/database/migrations/20231010A-add-extensions.d.ts +3 -0
- package/dist/database/migrations/20231010A-add-extensions.js +9 -0
- package/dist/database/run-ast.js +1 -1
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +5 -1
- package/dist/database/system-data/collections/collections.yaml +6 -0
- package/dist/database/system-data/fields/activity.yaml +4 -4
- package/dist/database/system-data/fields/collections.yaml +19 -0
- package/dist/database/system-data/fields/extensions.yaml +10 -0
- package/dist/database/system-data/fields/revisions.yaml +3 -0
- package/dist/database/system-data/fields/settings.yaml +73 -17
- package/dist/database/system-data/fields/users.yaml +50 -12
- package/dist/database/system-data/fields/versions.yaml +38 -0
- package/dist/database/system-data/fields/webhooks.yaml +9 -9
- package/dist/database/system-data/relations/relations.yaml +88 -20
- package/dist/emitter.d.ts +2 -2
- package/dist/env.js +4 -0
- package/dist/extensions/lib/get-extensions-settings.d.ts +7 -0
- package/dist/extensions/lib/get-extensions-settings.js +39 -0
- package/dist/extensions/lib/get-extensions.d.ts +1 -0
- package/dist/extensions/lib/get-extensions.js +11 -0
- package/dist/extensions/{get-shared-deps-mapping.js → lib/get-shared-deps-mapping.js} +3 -3
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +31 -0
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.js +80 -0
- package/dist/extensions/lib/sandbox/generate-host-function-reference.d.ts +11 -0
- package/dist/extensions/lib/sandbox/generate-host-function-reference.js +28 -0
- package/dist/extensions/lib/sandbox/register/action.d.ts +6 -0
- package/dist/extensions/lib/sandbox/register/action.js +18 -0
- package/dist/extensions/lib/sandbox/register/call-reference.d.ts +5 -0
- package/dist/extensions/lib/sandbox/register/call-reference.js +20 -0
- package/dist/extensions/lib/sandbox/register/filter.d.ts +6 -0
- package/dist/extensions/lib/sandbox/register/filter.js +21 -0
- package/dist/extensions/lib/sandbox/register/index.d.ts +5 -0
- package/dist/extensions/lib/sandbox/register/index.js +5 -0
- package/dist/extensions/lib/sandbox/register/operation.d.ts +6 -0
- package/dist/extensions/lib/sandbox/register/operation.js +19 -0
- package/dist/extensions/lib/sandbox/register/route.d.ts +17 -0
- package/dist/extensions/lib/sandbox/register/route.js +44 -0
- package/dist/extensions/lib/sandbox/sdk/generators/index.d.ts +3 -0
- package/dist/extensions/lib/sandbox/sdk/generators/index.js +3 -0
- package/dist/extensions/lib/sandbox/sdk/generators/log.d.ts +3 -0
- package/dist/extensions/lib/sandbox/sdk/generators/log.js +11 -0
- package/dist/extensions/lib/sandbox/sdk/generators/request.d.ts +12 -0
- package/dist/extensions/lib/sandbox/sdk/generators/request.js +49 -0
- package/dist/extensions/lib/sandbox/sdk/generators/sleep.d.ts +3 -0
- package/dist/extensions/lib/sandbox/sdk/generators/sleep.js +11 -0
- package/dist/extensions/lib/sandbox/sdk/index.d.ts +2 -0
- package/dist/extensions/lib/sandbox/sdk/index.js +2 -0
- package/dist/extensions/lib/sandbox/sdk/instantiate.d.ts +11 -0
- package/dist/extensions/lib/sandbox/sdk/instantiate.js +28 -0
- package/dist/extensions/lib/sandbox/sdk/sdk.d.ts +20 -0
- package/dist/extensions/lib/sandbox/sdk/sdk.js +11 -0
- package/dist/extensions/lib/sandbox/sdk/utils/index.d.ts +1 -0
- package/dist/extensions/lib/sandbox/sdk/utils/index.js +1 -0
- package/dist/extensions/lib/sandbox/sdk/utils/wrap.d.ts +11 -0
- package/dist/extensions/lib/sandbox/sdk/utils/wrap.js +17 -0
- package/dist/extensions/manager.d.ts +128 -14
- package/dist/extensions/manager.js +310 -136
- package/dist/extensions/types.d.ts +1 -5
- package/dist/flows.d.ts +1 -1
- package/dist/flows.js +6 -6
- package/dist/middleware/respond.js +12 -0
- package/dist/server.js +2 -1
- package/dist/services/assets.js +1 -1
- package/dist/services/extensions.d.ts +31 -0
- package/dist/services/extensions.js +136 -0
- package/dist/services/graphql/index.d.ts +1 -1
- package/dist/services/graphql/index.js +87 -24
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/server.js +3 -1
- package/dist/services/users.js +2 -0
- package/dist/services/versions.d.ts +21 -0
- package/dist/services/versions.js +238 -0
- package/dist/types/collection.d.ts +1 -0
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +30 -2
- package/dist/utils/delete-from-require-cache.d.ts +1 -0
- package/dist/utils/delete-from-require-cache.js +5 -0
- package/dist/utils/get-service.js +3 -1
- package/dist/utils/import-file-url.d.ts +5 -0
- package/dist/utils/import-file-url.js +6 -0
- package/dist/utils/job-queue.d.ts +2 -3
- package/dist/utils/redact-object.d.ts +1 -1
- package/dist/utils/redact-object.js +37 -24
- package/dist/utils/sanitize-query.js +3 -0
- package/dist/utils/validate-query.js +1 -0
- package/dist/worker-pool.js +8 -0
- package/package.json +28 -27
- package/dist/extensions/get-extensions.d.ts +0 -47
- package/dist/extensions/get-extensions.js +0 -9
- package/dist/extensions/normalize-extension-info.d.ts +0 -5
- package/dist/extensions/normalize-extension-info.js +0 -30
- /package/dist/extensions/{get-shared-deps-mapping.d.ts → lib/get-shared-deps-mapping.d.ts} +0 -0
- /package/dist/extensions/{wrap-embeds.d.ts → lib/wrap-embeds.d.ts} +0 -0
- /package/dist/extensions/{wrap-embeds.js → lib/wrap-embeds.js} +0 -0
|
@@ -8,9 +8,9 @@ import nodeResolveDefault from '@rollup/plugin-node-resolve';
|
|
|
8
8
|
import virtualDefault from '@rollup/plugin-virtual';
|
|
9
9
|
import chokidar, { FSWatcher } from 'chokidar';
|
|
10
10
|
import express, { Router } from 'express';
|
|
11
|
+
import ivm from 'isolated-vm';
|
|
11
12
|
import { clone } from 'lodash-es';
|
|
12
|
-
import { readdir } from 'node:fs/promises';
|
|
13
|
-
import { createRequire } from 'node:module';
|
|
13
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import path from 'path';
|
|
@@ -21,45 +21,90 @@ import env from '../env.js';
|
|
|
21
21
|
import { getFlowManager } from '../flows.js';
|
|
22
22
|
import logger from '../logger.js';
|
|
23
23
|
import * as services from '../services/index.js';
|
|
24
|
+
import { deleteFromRequireCache } from '../utils/delete-from-require-cache.js';
|
|
24
25
|
import getModuleDefault from '../utils/get-module-default.js';
|
|
25
26
|
import { getSchema } from '../utils/get-schema.js';
|
|
27
|
+
import { importFileUrl } from '../utils/import-file-url.js';
|
|
26
28
|
import { JobQueue } from '../utils/job-queue.js';
|
|
27
29
|
import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import { getExtensionsSettings } from './lib/get-extensions-settings.js';
|
|
31
|
+
import { getExtensions } from './lib/get-extensions.js';
|
|
32
|
+
import { getSharedDepsMapping } from './lib/get-shared-deps-mapping.js';
|
|
33
|
+
import { generateApiExtensionsSandboxEntrypoint } from './lib/sandbox/generate-api-extensions-sandbox-entrypoint.js';
|
|
34
|
+
import { instantiateSandboxSdk } from './lib/sandbox/sdk/instantiate.js';
|
|
35
|
+
import { wrapEmbeds } from './lib/wrap-embeds.js';
|
|
32
36
|
// Workaround for https://github.com/rollup/plugins/issues/1329
|
|
33
37
|
const virtual = virtualDefault;
|
|
34
38
|
const alias = aliasDefault;
|
|
35
39
|
const nodeResolve = nodeResolveDefault;
|
|
36
|
-
const require = createRequire(import.meta.url);
|
|
37
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
41
|
const defaultOptions = {
|
|
39
42
|
schedule: true,
|
|
40
43
|
watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
|
|
41
44
|
};
|
|
42
45
|
export class ExtensionManager {
|
|
46
|
+
options = defaultOptions;
|
|
47
|
+
/**
|
|
48
|
+
* Whether or not the extensions have been read from disk and registered into the system
|
|
49
|
+
*/
|
|
43
50
|
isLoaded = false;
|
|
44
|
-
|
|
51
|
+
/**
|
|
52
|
+
* All extensions that are loaded within the current process
|
|
53
|
+
*/
|
|
45
54
|
extensions = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Settings for the extensions that are loaded within the current process
|
|
57
|
+
*/
|
|
58
|
+
extensionsSettings = [];
|
|
59
|
+
/**
|
|
60
|
+
* App extensions rolled up into a single bundle. Any chunks from the bundle will be available
|
|
61
|
+
* under appExtensionChunks
|
|
62
|
+
*/
|
|
63
|
+
appExtensionsBundle = null;
|
|
64
|
+
/**
|
|
65
|
+
* Individual filename chunks from the rollup bundle. Used to improve the performance by allowing
|
|
66
|
+
* extensions to split up their bundle into multiple smaller chunks
|
|
67
|
+
*/
|
|
68
|
+
appExtensionChunks = new Map();
|
|
69
|
+
/**
|
|
70
|
+
* Callbacks to be able to unregister extensions
|
|
71
|
+
*/
|
|
72
|
+
unregisterFunctionMap = new Map();
|
|
73
|
+
/**
|
|
74
|
+
* A local-to-extensions scoped emitter that can be used to fire and listen to custom events
|
|
75
|
+
* between extensions. These events are completely isolated from the core events that trigger
|
|
76
|
+
* hooks etc
|
|
77
|
+
*/
|
|
78
|
+
localEmitter = new Emitter();
|
|
79
|
+
/**
|
|
80
|
+
* Locally scoped express router used for custom endpoints. Allows extensions to dynamically
|
|
81
|
+
* register and de-register endpoints without affecting the regular global router
|
|
82
|
+
*/
|
|
83
|
+
endpointRouter = Router();
|
|
84
|
+
/**
|
|
85
|
+
* Custom HTML to be injected at the end of the `<head>` tag of the app's index.html
|
|
86
|
+
*/
|
|
52
87
|
hookEmbedsHead = [];
|
|
88
|
+
/**
|
|
89
|
+
* Custom HTML to be injected at the end of the `<body>` tag of the app's index.html
|
|
90
|
+
*/
|
|
53
91
|
hookEmbedsBody = [];
|
|
54
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Used to prevent race conditions when reloading extensions. Forces each reload to happen in
|
|
94
|
+
* sequence.
|
|
95
|
+
*/
|
|
96
|
+
reloadQueue = new JobQueue();
|
|
97
|
+
/**
|
|
98
|
+
* Optional file system watcher to auto-reload extensions when the local file system changes
|
|
99
|
+
*/
|
|
55
100
|
watcher = null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Load and register all extensions
|
|
103
|
+
*
|
|
104
|
+
* @param {ExtensionManagerOptions} options - Extension manager configuration options
|
|
105
|
+
* @param {boolean} options.schedule - Whether or not to allow for scheduled (CRON) hook extensions
|
|
106
|
+
* @param {boolean} options.watch - Whether or not to watch the local extensions folder for changes
|
|
107
|
+
*/
|
|
63
108
|
async initialize(options = {}) {
|
|
64
109
|
this.options = {
|
|
65
110
|
...defaultOptions,
|
|
@@ -74,15 +119,48 @@ export class ExtensionManager {
|
|
|
74
119
|
}
|
|
75
120
|
if (!this.isLoaded) {
|
|
76
121
|
await this.load();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
logger.info(`Loaded extensions: ${loadedExtensions.map((ext) => ext.name).join(', ')}`);
|
|
122
|
+
if (this.extensions.length > 0) {
|
|
123
|
+
logger.info(`Loaded extensions: ${this.extensions.map((ext) => ext.name).join(', ')}`);
|
|
80
124
|
}
|
|
81
125
|
}
|
|
82
126
|
if (this.options.watch && !wasWatcherInitialized) {
|
|
83
127
|
this.updateWatchedExtensions(this.extensions);
|
|
84
128
|
}
|
|
85
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Load all extensions from disk and register them in their respective places
|
|
132
|
+
*/
|
|
133
|
+
async load() {
|
|
134
|
+
try {
|
|
135
|
+
await ensureExtensionDirs(env['EXTENSIONS_PATH'], NESTED_EXTENSION_TYPES);
|
|
136
|
+
this.extensions = await getExtensions();
|
|
137
|
+
this.extensionsSettings = await getExtensionsSettings(this.extensions);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.warn(`Couldn't load extensions`);
|
|
141
|
+
logger.warn(err);
|
|
142
|
+
}
|
|
143
|
+
await this.registerHooks();
|
|
144
|
+
await this.registerEndpoints();
|
|
145
|
+
await this.registerOperations();
|
|
146
|
+
await this.registerBundles();
|
|
147
|
+
if (env['SERVE_APP']) {
|
|
148
|
+
this.appExtensionsBundle = await this.generateExtensionBundle();
|
|
149
|
+
}
|
|
150
|
+
this.isLoaded = true;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Unregister all extensions from the current process
|
|
154
|
+
*/
|
|
155
|
+
async unload() {
|
|
156
|
+
await this.unregisterApiExtensions();
|
|
157
|
+
this.localEmitter.offAll();
|
|
158
|
+
this.appExtensionsBundle = null;
|
|
159
|
+
this.isLoaded = false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Reload all the extensions. Will unload if extensions have already been loaded
|
|
163
|
+
*/
|
|
86
164
|
reload() {
|
|
87
165
|
this.reloadQueue.enqueue(async () => {
|
|
88
166
|
if (this.isLoaded) {
|
|
@@ -107,57 +185,42 @@ export class ExtensionManager {
|
|
|
107
185
|
}
|
|
108
186
|
});
|
|
109
187
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return extensionInfo;
|
|
116
|
-
}
|
|
117
|
-
getExtension(name) {
|
|
118
|
-
return this.extensions.find((extension) => extension.name === name);
|
|
119
|
-
}
|
|
120
|
-
getAppExtensions() {
|
|
121
|
-
return this.appExtensions;
|
|
188
|
+
/**
|
|
189
|
+
* Return the previously generated app extensions bundle
|
|
190
|
+
*/
|
|
191
|
+
getAppExtensionsBundle() {
|
|
192
|
+
return this.appExtensionsBundle;
|
|
122
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Return the previously generated app extension bundle chunk by name
|
|
196
|
+
*/
|
|
123
197
|
getAppExtensionChunk(name) {
|
|
124
198
|
return this.appExtensionChunks.get(name) ?? null;
|
|
125
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Return the scoped router for custom endpoints
|
|
202
|
+
*/
|
|
126
203
|
getEndpointRouter() {
|
|
127
204
|
return this.endpointRouter;
|
|
128
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Return the custom HTML head and body embeds wrapped in a marker comment
|
|
208
|
+
*/
|
|
129
209
|
getEmbeds() {
|
|
130
210
|
return {
|
|
131
211
|
head: wrapEmbeds('Custom Embed Head', this.hookEmbedsHead),
|
|
132
212
|
body: wrapEmbeds('Custom Embed Body', this.hookEmbedsBody),
|
|
133
213
|
};
|
|
134
214
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
catch (err) {
|
|
141
|
-
logger.warn(`Couldn't load extensions`);
|
|
142
|
-
logger.warn(err);
|
|
143
|
-
}
|
|
144
|
-
await this.registerHooks();
|
|
145
|
-
await this.registerEndpoints();
|
|
146
|
-
await this.registerOperations();
|
|
147
|
-
await this.registerBundles();
|
|
148
|
-
if (env['SERVE_APP']) {
|
|
149
|
-
this.appExtensions = await this.generateExtensionBundle();
|
|
150
|
-
}
|
|
151
|
-
this.isLoaded = true;
|
|
152
|
-
}
|
|
153
|
-
async unload() {
|
|
154
|
-
await this.unregisterApiExtensions();
|
|
155
|
-
this.apiEmitter.offAll();
|
|
156
|
-
if (env['SERVE_APP']) {
|
|
157
|
-
this.appExtensions = null;
|
|
158
|
-
}
|
|
159
|
-
this.isLoaded = false;
|
|
215
|
+
/**
|
|
216
|
+
* Allow reading the installed extensions
|
|
217
|
+
*/
|
|
218
|
+
getExtensions() {
|
|
219
|
+
return this.extensions;
|
|
160
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Start the chokidar watcher for extensions on the local filesystem
|
|
223
|
+
*/
|
|
161
224
|
initializeWatcher() {
|
|
162
225
|
logger.info('Watching extensions for changes...');
|
|
163
226
|
const extensionDirUrl = pathToRelativeUrl(env['EXTENSIONS_PATH']);
|
|
@@ -181,12 +244,19 @@ export class ExtensionManager {
|
|
|
181
244
|
.on('change', () => this.reload())
|
|
182
245
|
.on('unlink', () => this.reload());
|
|
183
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Close and destroy the local filesystem watcher if enabled
|
|
249
|
+
*/
|
|
184
250
|
async closeWatcher() {
|
|
185
251
|
if (this.watcher) {
|
|
186
252
|
await this.watcher.close();
|
|
187
253
|
this.watcher = null;
|
|
188
254
|
}
|
|
189
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Update the chokidar watcher configuration when new extensions are added or existing ones
|
|
258
|
+
* removed
|
|
259
|
+
*/
|
|
190
260
|
updateWatchedExtensions(added, removed = []) {
|
|
191
261
|
if (this.watcher) {
|
|
192
262
|
const toPackageExtensionPaths = (extensions) => extensions
|
|
@@ -203,6 +273,10 @@ export class ExtensionManager {
|
|
|
203
273
|
this.watcher.unwatch(removedPackageExtensionPaths);
|
|
204
274
|
}
|
|
205
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Uses rollup to bundle the app extensions together into a single file the app can download and
|
|
278
|
+
* run.
|
|
279
|
+
*/
|
|
206
280
|
async generateExtensionBundle() {
|
|
207
281
|
const sharedDepsMapping = await getSharedDepsMapping(APP_SHARED_DEPS);
|
|
208
282
|
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
|
|
@@ -232,15 +306,66 @@ export class ExtensionManager {
|
|
|
232
306
|
}
|
|
233
307
|
return null;
|
|
234
308
|
}
|
|
309
|
+
async registerSandboxedApiExtension(extension) {
|
|
310
|
+
const sandboxMemory = Number(env['EXTENSIONS_SANDBOX_MEMORY']);
|
|
311
|
+
const sandboxTimeout = Number(env['EXTENSIONS_SANDBOX_TIMEOUT']);
|
|
312
|
+
const entrypointPath = path.resolve(extension.path, isTypeIn(extension, HYBRID_EXTENSION_TYPES) ? extension.entrypoint.api : extension.entrypoint);
|
|
313
|
+
const extensionCode = await readFile(entrypointPath, 'utf-8');
|
|
314
|
+
const isolate = new ivm.Isolate({
|
|
315
|
+
memoryLimit: sandboxMemory,
|
|
316
|
+
onCatastrophicError: (e) => {
|
|
317
|
+
logger.error(`Error in API extension sandbox of ${extension.type} "${extension.name}"`);
|
|
318
|
+
logger.error(e);
|
|
319
|
+
process.abort();
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const context = await isolate.createContext();
|
|
323
|
+
const module = await isolate.compileModule(extensionCode, { filename: `file://${entrypointPath}` });
|
|
324
|
+
const sdkModule = await instantiateSandboxSdk(isolate, extension.sandbox?.requestedScopes ?? {});
|
|
325
|
+
await module.instantiate(context, (specifier) => {
|
|
326
|
+
if (specifier !== 'directus:api') {
|
|
327
|
+
throw new Error('Imports other than "directus:api" are prohibited in API extension sandboxes');
|
|
328
|
+
}
|
|
329
|
+
return sdkModule;
|
|
330
|
+
});
|
|
331
|
+
await module.evaluate({ timeout: sandboxTimeout });
|
|
332
|
+
const cb = await module.namespace.get('default', { reference: true });
|
|
333
|
+
const { code, hostFunctions, unregisterFunction } = generateApiExtensionsSandboxEntrypoint(extension.type, extension.name, this.endpointRouter);
|
|
334
|
+
await context.evalClosure(code, [cb, ...hostFunctions.map((fn) => new ivm.Reference(fn))], {
|
|
335
|
+
timeout: sandboxTimeout,
|
|
336
|
+
filename: '<extensions-sandbox>',
|
|
337
|
+
});
|
|
338
|
+
this.unregisterFunctionMap.set(extension.name, async () => {
|
|
339
|
+
await unregisterFunction();
|
|
340
|
+
isolate.dispose();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Import the hook module code for all hook extensions, and register them individually through
|
|
345
|
+
* registerHook
|
|
346
|
+
*/
|
|
235
347
|
async registerHooks() {
|
|
236
348
|
const hooks = this.extensions.filter((extension) => extension.type === 'hook');
|
|
237
349
|
for (const hook of hooks) {
|
|
350
|
+
const { enabled } = this.extensionsSettings.find(({ name }) => name === hook.name) ?? { enabled: false };
|
|
351
|
+
if (!enabled)
|
|
352
|
+
continue;
|
|
238
353
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
354
|
+
if (hook.sandbox?.enabled) {
|
|
355
|
+
await this.registerSandboxedApiExtension(hook);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const hookPath = path.resolve(hook.path, hook.entrypoint);
|
|
359
|
+
const hookInstance = await importFileUrl(hookPath, import.meta.url, {
|
|
360
|
+
fresh: true,
|
|
361
|
+
});
|
|
362
|
+
const config = getModuleDefault(hookInstance);
|
|
363
|
+
const unregisterFunctions = this.registerHook(config, hook.name);
|
|
364
|
+
this.unregisterFunctionMap.set(hook.name, async () => {
|
|
365
|
+
await Promise.all(unregisterFunctions.map((fn) => fn()));
|
|
366
|
+
deleteFromRequireCache(hookPath);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
244
369
|
}
|
|
245
370
|
catch (error) {
|
|
246
371
|
logger.warn(`Couldn't register hook "${hook.name}"`);
|
|
@@ -248,15 +373,32 @@ export class ExtensionManager {
|
|
|
248
373
|
}
|
|
249
374
|
}
|
|
250
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Import the endpoint module code for all endpoint extensions, and register them individually through
|
|
378
|
+
* registerEndpoint
|
|
379
|
+
*/
|
|
251
380
|
async registerEndpoints() {
|
|
252
381
|
const endpoints = this.extensions.filter((extension) => extension.type === 'endpoint');
|
|
253
382
|
for (const endpoint of endpoints) {
|
|
383
|
+
const { enabled } = this.extensionsSettings.find(({ name }) => name === endpoint.name) ?? { enabled: false };
|
|
384
|
+
if (!enabled)
|
|
385
|
+
continue;
|
|
254
386
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
387
|
+
if (endpoint.sandbox?.enabled) {
|
|
388
|
+
await this.registerSandboxedApiExtension(endpoint);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint);
|
|
392
|
+
const endpointInstance = await importFileUrl(endpointPath, import.meta.url, {
|
|
393
|
+
fresh: true,
|
|
394
|
+
});
|
|
395
|
+
const config = getModuleDefault(endpointInstance);
|
|
396
|
+
const unregister = this.registerEndpoint(config, endpoint.name);
|
|
397
|
+
this.unregisterFunctionMap.set(endpoint.name, async () => {
|
|
398
|
+
await unregister();
|
|
399
|
+
deleteFromRequireCache(endpointPath);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
260
402
|
}
|
|
261
403
|
catch (error) {
|
|
262
404
|
logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
|
|
@@ -264,6 +406,10 @@ export class ExtensionManager {
|
|
|
264
406
|
}
|
|
265
407
|
}
|
|
266
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Import the operation module code for all operation extensions, and register them individually through
|
|
411
|
+
* registerOperation
|
|
412
|
+
*/
|
|
267
413
|
async registerOperations() {
|
|
268
414
|
const internalOperations = await readdir(path.join(__dirname, '..', 'operations'));
|
|
269
415
|
for (const operation of internalOperations) {
|
|
@@ -273,12 +419,25 @@ export class ExtensionManager {
|
|
|
273
419
|
}
|
|
274
420
|
const operations = this.extensions.filter((extension) => extension.type === 'operation');
|
|
275
421
|
for (const operation of operations) {
|
|
422
|
+
const { enabled } = this.extensionsSettings.find(({ name }) => name === operation.name) ?? { enabled: false };
|
|
423
|
+
if (!enabled)
|
|
424
|
+
continue;
|
|
276
425
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
426
|
+
if (operation.sandbox?.enabled) {
|
|
427
|
+
await this.registerSandboxedApiExtension(operation);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const operationPath = path.resolve(operation.path, operation.entrypoint.api);
|
|
431
|
+
const operationInstance = await importFileUrl(operationPath, import.meta.url, {
|
|
432
|
+
fresh: true,
|
|
433
|
+
});
|
|
434
|
+
const config = getModuleDefault(operationInstance);
|
|
435
|
+
const unregister = this.registerOperation(config);
|
|
436
|
+
this.unregisterFunctionMap.set(operation.name, async () => {
|
|
437
|
+
await unregister();
|
|
438
|
+
deleteFromRequireCache(operationPath);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
282
441
|
}
|
|
283
442
|
catch (error) {
|
|
284
443
|
logger.warn(`Couldn't register operation "${operation.name}"`);
|
|
@@ -286,23 +445,36 @@ export class ExtensionManager {
|
|
|
286
445
|
}
|
|
287
446
|
}
|
|
288
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Import the module code for all hook, endpoint, and operation extensions registered within a
|
|
450
|
+
* bundle, and register them with their respective registration function
|
|
451
|
+
*/
|
|
289
452
|
async registerBundles() {
|
|
290
453
|
const bundles = this.extensions.filter((extension) => extension.type === 'bundle');
|
|
291
454
|
for (const bundle of bundles) {
|
|
292
455
|
try {
|
|
293
456
|
const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
|
|
294
|
-
const bundleInstances = await
|
|
457
|
+
const bundleInstances = await importFileUrl(bundlePath, import.meta.url, {
|
|
458
|
+
fresh: true,
|
|
459
|
+
});
|
|
295
460
|
const configs = getModuleDefault(bundleInstances);
|
|
461
|
+
const unregisterFunctions = [];
|
|
296
462
|
for (const { config, name } of configs.hooks) {
|
|
297
|
-
this.registerHook(config, name);
|
|
463
|
+
const unregisters = this.registerHook(config, name);
|
|
464
|
+
unregisterFunctions.push(...unregisters);
|
|
298
465
|
}
|
|
299
466
|
for (const { config, name } of configs.endpoints) {
|
|
300
|
-
this.registerEndpoint(config, name);
|
|
467
|
+
const unregister = this.registerEndpoint(config, name);
|
|
468
|
+
unregisterFunctions.push(unregister);
|
|
301
469
|
}
|
|
302
470
|
for (const { config } of configs.operations) {
|
|
303
|
-
this.registerOperation(config);
|
|
471
|
+
const unregister = this.registerOperation(config);
|
|
472
|
+
unregisterFunctions.push(unregister);
|
|
304
473
|
}
|
|
305
|
-
this.
|
|
474
|
+
this.unregisterFunctionMap.set(bundle.name, async () => {
|
|
475
|
+
await Promise.all(unregisterFunctions.map((fn) => fn()));
|
|
476
|
+
deleteFromRequireCache(bundlePath);
|
|
477
|
+
});
|
|
306
478
|
}
|
|
307
479
|
catch (error) {
|
|
308
480
|
logger.warn(`Couldn't register bundle "${bundle.name}"`);
|
|
@@ -310,31 +482,29 @@ export class ExtensionManager {
|
|
|
310
482
|
}
|
|
311
483
|
}
|
|
312
484
|
}
|
|
313
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Register a single hook
|
|
487
|
+
*/
|
|
488
|
+
registerHook(hookRegistrationCallback, name) {
|
|
314
489
|
let scheduleIndex = 0;
|
|
315
|
-
const
|
|
490
|
+
const unregisterFunctions = [];
|
|
491
|
+
const hookRegistrationContext = {
|
|
316
492
|
filter: (event, handler) => {
|
|
317
493
|
emitter.onFilter(event, handler);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
name: event,
|
|
321
|
-
handler,
|
|
494
|
+
unregisterFunctions.push(() => {
|
|
495
|
+
emitter.offFilter(event, handler);
|
|
322
496
|
});
|
|
323
497
|
},
|
|
324
498
|
action: (event, handler) => {
|
|
325
499
|
emitter.onAction(event, handler);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
name: event,
|
|
329
|
-
handler,
|
|
500
|
+
unregisterFunctions.push(() => {
|
|
501
|
+
emitter.offAction(event, handler);
|
|
330
502
|
});
|
|
331
503
|
},
|
|
332
504
|
init: (event, handler) => {
|
|
333
505
|
emitter.onInit(event, handler);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
name: event,
|
|
337
|
-
handler,
|
|
506
|
+
unregisterFunctions.push(() => {
|
|
507
|
+
emitter.offInit(name, handler);
|
|
338
508
|
});
|
|
339
509
|
},
|
|
340
510
|
schedule: (cron, handler) => {
|
|
@@ -350,9 +520,8 @@ export class ExtensionManager {
|
|
|
350
520
|
}
|
|
351
521
|
});
|
|
352
522
|
scheduleIndex++;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
job,
|
|
523
|
+
unregisterFunctions.push(async () => {
|
|
524
|
+
await job.stop();
|
|
356
525
|
});
|
|
357
526
|
}
|
|
358
527
|
else {
|
|
@@ -361,69 +530,74 @@ export class ExtensionManager {
|
|
|
361
530
|
},
|
|
362
531
|
embed: (position, code) => {
|
|
363
532
|
const content = typeof code === 'function' ? code() : code;
|
|
364
|
-
if (content.trim().length
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
533
|
+
if (content.trim().length !== 0) {
|
|
534
|
+
if (position === 'head') {
|
|
535
|
+
const index = this.hookEmbedsHead.length;
|
|
536
|
+
this.hookEmbedsHead.push(content);
|
|
537
|
+
unregisterFunctions.push(() => {
|
|
538
|
+
this.hookEmbedsHead.splice(index, 1);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
const index = this.hookEmbedsBody.length;
|
|
543
|
+
this.hookEmbedsBody.push(content);
|
|
544
|
+
unregisterFunctions.push(() => {
|
|
545
|
+
this.hookEmbedsBody.splice(index, 1);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
370
548
|
}
|
|
371
|
-
|
|
372
|
-
|
|
549
|
+
else {
|
|
550
|
+
logger.warn(`Couldn't register embed hook. Provided code is empty!`);
|
|
373
551
|
}
|
|
374
552
|
},
|
|
375
553
|
};
|
|
376
|
-
|
|
554
|
+
hookRegistrationCallback(hookRegistrationContext, {
|
|
377
555
|
services,
|
|
378
556
|
env,
|
|
379
557
|
database: getDatabase(),
|
|
380
|
-
emitter: this.
|
|
558
|
+
emitter: this.localEmitter,
|
|
381
559
|
logger,
|
|
382
560
|
getSchema,
|
|
383
561
|
});
|
|
562
|
+
return unregisterFunctions;
|
|
384
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Register an individual endpoint
|
|
566
|
+
*/
|
|
385
567
|
registerEndpoint(config, name) {
|
|
386
|
-
const
|
|
568
|
+
const endpointRegistrationCallback = typeof config === 'function' ? config : config.handler;
|
|
387
569
|
const routeName = typeof config === 'function' ? name : config.id;
|
|
388
570
|
const scopedRouter = express.Router();
|
|
389
571
|
this.endpointRouter.use(`/${routeName}`, scopedRouter);
|
|
390
|
-
|
|
572
|
+
endpointRegistrationCallback(scopedRouter, {
|
|
391
573
|
services,
|
|
392
574
|
env,
|
|
393
575
|
database: getDatabase(),
|
|
394
|
-
emitter: this.
|
|
576
|
+
emitter: this.localEmitter,
|
|
395
577
|
logger,
|
|
396
578
|
getSchema,
|
|
397
579
|
});
|
|
580
|
+
const unregisterFunction = () => {
|
|
581
|
+
this.endpointRouter.stack = this.endpointRouter.stack.filter((layer) => scopedRouter !== layer.handle);
|
|
582
|
+
};
|
|
583
|
+
return unregisterFunction;
|
|
398
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Register an individual operation
|
|
587
|
+
*/
|
|
399
588
|
registerOperation(config) {
|
|
400
589
|
const flowManager = getFlowManager();
|
|
401
590
|
flowManager.addOperation(config.id, config.handler);
|
|
591
|
+
const unregisterFunction = () => {
|
|
592
|
+
flowManager.removeOperation(config.id);
|
|
593
|
+
};
|
|
594
|
+
return unregisterFunction;
|
|
402
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Remove the registration for all API extensions
|
|
598
|
+
*/
|
|
403
599
|
async unregisterApiExtensions() {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
case 'filter':
|
|
407
|
-
emitter.offFilter(event.name, event.handler);
|
|
408
|
-
break;
|
|
409
|
-
case 'action':
|
|
410
|
-
emitter.offAction(event.name, event.handler);
|
|
411
|
-
break;
|
|
412
|
-
case 'init':
|
|
413
|
-
emitter.offInit(event.name, event.handler);
|
|
414
|
-
break;
|
|
415
|
-
case 'schedule':
|
|
416
|
-
await event.job.stop();
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
this.hookEvents = [];
|
|
421
|
-
this.endpointRouter.stack = [];
|
|
422
|
-
const flowManager = getFlowManager();
|
|
423
|
-
flowManager.clearOperations();
|
|
424
|
-
for (const apiExtension of this.apiExtensions) {
|
|
425
|
-
delete require.cache[require.resolve(apiExtension.path)];
|
|
426
|
-
}
|
|
427
|
-
this.apiExtensions = [];
|
|
600
|
+
const unregisterFunctions = Array.from(this.unregisterFunctionMap.values());
|
|
601
|
+
await Promise.all(unregisterFunctions.map((fn) => fn()));
|
|
428
602
|
}
|
|
429
603
|
}
|
|
@@ -13,11 +13,7 @@ export type BundleConfig = {
|
|
|
13
13
|
config: OperationApiConfig;
|
|
14
14
|
}[];
|
|
15
15
|
};
|
|
16
|
-
export
|
|
17
|
-
export type ApiExtensions = {
|
|
18
|
-
path: string;
|
|
19
|
-
}[];
|
|
20
|
-
export interface Options {
|
|
16
|
+
export interface ExtensionManagerOptions {
|
|
21
17
|
schedule: boolean;
|
|
22
18
|
watch: boolean;
|
|
23
19
|
}
|
package/dist/flows.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare class FlowManager {
|
|
|
12
12
|
initialize(): Promise<void>;
|
|
13
13
|
reload(): Promise<void>;
|
|
14
14
|
addOperation(id: string, operation: OperationHandler): void;
|
|
15
|
-
|
|
15
|
+
removeOperation(id: string): void;
|
|
16
16
|
runOperationFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
|
|
17
17
|
runWebhookFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<{
|
|
18
18
|
result: unknown;
|