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