@directus/api 14.1.1 → 15.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.
Files changed (78) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cli/commands/schema/apply.js +44 -33
  5. package/dist/cli/index.js +2 -2
  6. package/dist/database/index.d.ts +2 -1
  7. package/dist/database/index.js +2 -1
  8. package/dist/database/system-data/fields/settings.yaml +4 -0
  9. package/dist/database/system-data/fields/users.yaml +4 -0
  10. package/dist/database/system-data/relations/relations.yaml +4 -0
  11. package/dist/emitter.d.ts +1 -0
  12. package/dist/emitter.js +2 -1
  13. package/dist/env.d.ts +1 -0
  14. package/dist/env.js +6 -0
  15. package/dist/extensions/lib/get-shared-deps-mapping.js +1 -1
  16. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -0
  17. package/dist/extensions/manager.d.ts +5 -0
  18. package/dist/extensions/manager.js +42 -16
  19. package/dist/logger.d.ts +2 -1
  20. package/dist/logger.js +1 -0
  21. package/dist/middleware/rate-limiter-ip.js +14 -11
  22. package/dist/redis/create-redis.d.ts +7 -0
  23. package/dist/redis/create-redis.js +12 -0
  24. package/dist/redis/index.d.ts +2 -0
  25. package/dist/redis/index.js +2 -0
  26. package/dist/redis/use-redis.d.ts +16 -0
  27. package/dist/redis/use-redis.js +22 -0
  28. package/dist/server.d.ts +2 -0
  29. package/dist/services/extensions.js +1 -1
  30. package/dist/services/graphql/index.js +50 -15
  31. package/dist/services/payload.js +3 -3
  32. package/dist/services/server.js +2 -3
  33. package/dist/services/specifications.js +3 -3
  34. package/dist/services/users.js +6 -6
  35. package/dist/telemetry/index.d.ts +4 -0
  36. package/dist/telemetry/index.js +4 -0
  37. package/dist/telemetry/lib/get-report.d.ts +5 -0
  38. package/dist/telemetry/lib/get-report.js +42 -0
  39. package/dist/telemetry/lib/init-telemetry.d.ts +11 -0
  40. package/dist/telemetry/lib/init-telemetry.js +30 -0
  41. package/dist/telemetry/lib/send-report.d.ts +5 -0
  42. package/dist/telemetry/lib/send-report.js +23 -0
  43. package/dist/telemetry/lib/track.d.ts +10 -0
  44. package/dist/telemetry/lib/track.js +31 -0
  45. package/dist/telemetry/types/report.d.ts +58 -0
  46. package/dist/telemetry/types/report.js +1 -0
  47. package/dist/telemetry/utils/get-item-count.d.ts +26 -0
  48. package/dist/telemetry/utils/get-item-count.js +36 -0
  49. package/dist/telemetry/utils/get-random-wait-time.d.ts +5 -0
  50. package/dist/telemetry/utils/get-random-wait-time.js +5 -0
  51. package/dist/telemetry/utils/get-user-count.d.ts +7 -0
  52. package/dist/telemetry/utils/get-user-count.js +30 -0
  53. package/dist/telemetry/utils/get-user-item-count.d.ts +13 -0
  54. package/dist/telemetry/utils/get-user-item-count.js +18 -0
  55. package/dist/utils/apply-query.js +13 -2
  56. package/dist/utils/get-cache-key.js +1 -1
  57. package/dist/utils/get-ip-from-req.d.ts +1 -1
  58. package/dist/utils/get-ip-from-req.js +1 -1
  59. package/dist/utils/get-permissions.js +0 -3
  60. package/dist/utils/get-snapshot-diff.js +2 -1
  61. package/dist/utils/get-snapshot.js +1 -1
  62. package/dist/utils/get-versioned-hash.js +1 -1
  63. package/dist/utils/md.d.ts +1 -1
  64. package/dist/utils/md.js +3 -2
  65. package/dist/utils/merge-permissions.js +19 -11
  66. package/dist/utils/validate-query.js +1 -0
  67. package/dist/utils/validate-snapshot.js +3 -3
  68. package/dist/websocket/controllers/base.d.ts +2 -0
  69. package/dist/websocket/controllers/graphql.d.ts +2 -0
  70. package/dist/websocket/controllers/graphql.js +1 -1
  71. package/dist/websocket/controllers/index.d.ts +2 -0
  72. package/dist/websocket/controllers/rest.d.ts +2 -0
  73. package/dist/websocket/types.d.ts +3 -1
  74. package/package.json +108 -109
  75. package/dist/utils/package.d.ts +0 -2
  76. package/dist/utils/package.js +0 -6
  77. package/dist/utils/telemetry.d.ts +0 -1
  78. package/dist/utils/telemetry.js +0 -23
package/dist/app.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
1
2
  import { handlePressure } from '@directus/pressure';
2
3
  import cookieParser from 'cookie-parser';
3
4
  import express from 'express';
@@ -40,7 +41,6 @@ import webhooksRouter from './controllers/webhooks.js';
40
41
  import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations, } from './database/index.js';
41
42
  import emitter from './emitter.js';
42
43
  import env from './env.js';
43
- import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
44
44
  import { getExtensionManager } from './extensions/index.js';
45
45
  import { getFlowManager } from './flows.js';
46
46
  import logger, { expressLogger } from './logger.js';
@@ -56,11 +56,11 @@ import rateLimiter from './middleware/rate-limiter-ip.js';
56
56
  import sanitizeQuery from './middleware/sanitize-query.js';
57
57
  import schema from './middleware/schema.js';
58
58
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
59
- import { collectTelemetry } from './utils/telemetry.js';
60
59
  import { Url } from './utils/url.js';
61
60
  import { validateEnv } from './utils/validate-env.js';
62
61
  import { validateStorage } from './utils/validate-storage.js';
63
62
  import { init as initWebhooks } from './webhooks.js';
63
+ import { initTelemetry } from './telemetry/index.js';
64
64
  const require = createRequire(import.meta.url);
65
65
  export default async function createApp() {
66
66
  const helmet = await import('helmet');
@@ -234,7 +234,7 @@ export default async function createApp() {
234
234
  await emitter.emitInit('routes.after', { app });
235
235
  // Register all webhooks
236
236
  await initWebhooks();
237
- collectTelemetry();
237
+ initTelemetry();
238
238
  await emitter.emitInit('app.after', { app });
239
239
  return app;
240
240
  }
@@ -1,14 +1,13 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, InvalidCredentialsError, InvalidProviderConfigError, InvalidProviderError, InvalidTokenError, isDirectusError, ServiceUnavailableError, } from '@directus/errors';
2
2
  import { parseJSON } from '@directus/utils';
3
3
  import express, { Router } from 'express';
4
- import flatten from 'flat';
4
+ import { flatten } from 'flat';
5
5
  import jwt from 'jsonwebtoken';
6
6
  import { errors, generators, Issuer } from 'openid-client';
7
7
  import { getAuthProvider } from '../../auth.js';
8
8
  import getDatabase from '../../database/index.js';
9
9
  import emitter from '../../emitter.js';
10
10
  import env from '../../env.js';
11
- import { ErrorCode, InvalidCredentialsError, InvalidProviderError, InvalidProviderConfigError, InvalidTokenError, ServiceUnavailableError, } from '@directus/errors';
12
11
  import logger from '../../logger.js';
13
12
  import { respond } from '../../middleware/respond.js';
14
13
  import { AuthenticationService } from '../../services/authentication.js';
@@ -1,14 +1,13 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, InvalidCredentialsError, InvalidProviderConfigError, InvalidProviderError, InvalidTokenError, isDirectusError, ServiceUnavailableError, } from '@directus/errors';
2
2
  import { parseJSON } from '@directus/utils';
3
3
  import express, { Router } from 'express';
4
- import flatten from 'flat';
4
+ import { flatten } from 'flat';
5
5
  import jwt from 'jsonwebtoken';
6
6
  import { errors, generators, Issuer } from 'openid-client';
7
7
  import { getAuthProvider } from '../../auth.js';
8
8
  import getDatabase from '../../database/index.js';
9
9
  import emitter from '../../emitter.js';
10
10
  import env from '../../env.js';
11
- import { ErrorCode, InvalidCredentialsError, InvalidProviderError, InvalidProviderConfigError, InvalidTokenError, ServiceUnavailableError, } from '@directus/errors';
12
11
  import logger from '../../logger.js';
13
12
  import { respond } from '../../middleware/respond.js';
14
13
  import { AuthenticationService } from '../../services/authentication.js';
@@ -41,92 +41,90 @@ export async function apply(snapshotPath, options) {
41
41
  const dryRun = options?.dryRun === true;
42
42
  const promptForChanges = !dryRun && options?.yes !== true;
43
43
  if (dryRun || promptForChanges) {
44
- let message = '';
44
+ const sections = [];
45
45
  if (snapshotDiff.collections.length > 0) {
46
- message += chalk.black.underline.bold('Collections:');
46
+ const lines = [chalk.underline.bold('Collections:')];
47
47
  for (const { collection, diff } of snapshotDiff.collections) {
48
48
  if (diff[0]?.kind === DiffKind.EDIT) {
49
- message += `\n - ${chalk.blue('Update')} ${collection}`;
49
+ lines.push(` - ${chalk.magenta('Update')} ${collection}`);
50
50
  for (const change of diff) {
51
51
  if (change.kind === DiffKind.EDIT) {
52
- const path = change.path.slice(1).join('.');
53
- message += `\n - Set ${path} to ${change.rhs}`;
52
+ const path = formatPath(change.path);
53
+ lines.push(` - Set ${path} to ${change.rhs}`);
54
54
  }
55
55
  }
56
56
  }
57
57
  else if (diff[0]?.kind === DiffKind.DELETE) {
58
- message += `\n - ${chalk.red('Delete')} ${collection}`;
58
+ lines.push(` - ${chalk.red('Delete')} ${collection}`);
59
59
  }
60
60
  else if (diff[0]?.kind === DiffKind.NEW) {
61
- message += `\n - ${chalk.green('Create')} ${collection}`;
61
+ lines.push(` - ${chalk.green('Create')} ${collection}`);
62
62
  }
63
63
  else if (diff[0]?.kind === DiffKind.ARRAY) {
64
- message += `\n - ${chalk.blue('Update')} ${collection}`;
64
+ lines.push(` - ${chalk.magenta('Update')} ${collection}`);
65
65
  }
66
66
  }
67
+ sections.push(lines.join('\n'));
67
68
  }
68
69
  if (snapshotDiff.fields.length > 0) {
69
- message += '\n\n' + chalk.black.underline.bold('Fields:');
70
+ const lines = [chalk.underline.bold('Fields:')];
70
71
  for (const { collection, field, diff } of snapshotDiff.fields) {
71
72
  if (diff[0]?.kind === DiffKind.EDIT || isNestedMetaUpdate(diff[0])) {
72
- message += `\n - ${chalk.blue('Update')} ${collection}.${field}`;
73
+ lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
73
74
  for (const change of diff) {
74
- const path = change.path.slice(1).join('.');
75
+ const path = formatPath(change.path);
75
76
  if (change.kind === DiffKind.EDIT) {
76
- message += `\n - Set ${path} to ${change.rhs}`;
77
+ lines.push(` - Set ${path} to ${change.rhs}`);
77
78
  }
78
79
  else if (change.kind === DiffKind.DELETE) {
79
- message += `\n - Remove ${path}`;
80
+ lines.push(` - Remove ${path}`);
80
81
  }
81
82
  else if (change.kind === DiffKind.NEW) {
82
- message += `\n - Add ${path} and set it to ${change.rhs}`;
83
+ lines.push(` - Add ${path} and set it to ${change.rhs}`);
83
84
  }
84
85
  }
85
86
  }
86
87
  else if (diff[0]?.kind === DiffKind.DELETE) {
87
- message += `\n - ${chalk.red('Delete')} ${collection}.${field}`;
88
+ lines.push(` - ${chalk.red('Delete')} ${collection}.${field}`);
88
89
  }
89
90
  else if (diff[0]?.kind === DiffKind.NEW) {
90
- message += `\n - ${chalk.green('Create')} ${collection}.${field}`;
91
+ lines.push(` - ${chalk.green('Create')} ${collection}.${field}`);
91
92
  }
92
93
  else if (diff[0]?.kind === DiffKind.ARRAY) {
93
- message += `\n - ${chalk.blue('Update')} ${collection}.${field}`;
94
+ lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
94
95
  }
95
96
  }
97
+ sections.push(lines.join('\n'));
96
98
  }
97
99
  if (snapshotDiff.relations.length > 0) {
98
- message += '\n\n' + chalk.black.underline.bold('Relations:');
100
+ const lines = [chalk.underline.bold('Relations:')];
99
101
  for (const { collection, field, related_collection, diff } of snapshotDiff.relations) {
102
+ const relatedCollection = formatRelatedCollection(related_collection);
100
103
  if (diff[0]?.kind === DiffKind.EDIT) {
101
- message += `\n - ${chalk.blue('Update')} ${collection}.${field}`;
104
+ lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}${relatedCollection}`);
102
105
  for (const change of diff) {
103
106
  if (change.kind === DiffKind.EDIT) {
104
- const path = change.path.slice(1).join('.');
105
- message += `\n - Set ${path} to ${change.rhs}`;
107
+ const path = formatPath(change.path);
108
+ lines.push(` - Set ${path} to ${change.rhs}`);
106
109
  }
107
110
  }
108
111
  }
109
112
  else if (diff[0]?.kind === DiffKind.DELETE) {
110
- message += `\n - ${chalk.red('Delete')} ${collection}.${field}`;
113
+ lines.push(` - ${chalk.red('Delete')} ${collection}.${field}${relatedCollection}`);
111
114
  }
112
115
  else if (diff[0]?.kind === DiffKind.NEW) {
113
- message += `\n - ${chalk.green('Create')} ${collection}.${field}`;
116
+ lines.push(` - ${chalk.green('Create')} ${collection}.${field}${relatedCollection}`);
114
117
  }
115
118
  else if (diff[0]?.kind === DiffKind.ARRAY) {
116
- message += `\n - ${chalk.blue('Update')} ${collection}.${field}`;
117
- }
118
- else {
119
- continue;
120
- }
121
- // Related collection doesn't exist for a2o relationship types
122
- if (related_collection) {
123
- message += `-> ${related_collection}`;
119
+ lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}${relatedCollection}`);
124
120
  }
125
121
  }
122
+ sections.push(lines.join('\n'));
126
123
  }
127
- message = 'The following changes will be applied:\n\n' + chalk.black(message);
124
+ const message = 'The following changes will be applied:\n\n' + sections.join('\n\n');
128
125
  if (dryRun) {
129
- logger.info(message);
126
+ // eslint-disable-next-line no-console
127
+ console.log(message);
130
128
  process.exit(0);
131
129
  }
132
130
  const { proceed } = await inquirer.prompt([
@@ -151,3 +149,16 @@ export async function apply(snapshotPath, options) {
151
149
  process.exit(1);
152
150
  }
153
151
  }
152
+ function formatPath(path) {
153
+ if (path.length === 1) {
154
+ return path.toString();
155
+ }
156
+ return path.slice(1).join('.');
157
+ }
158
+ function formatRelatedCollection(relatedCollection) {
159
+ // Related collection doesn't exist for a2o relationship types
160
+ if (relatedCollection) {
161
+ return ` → ${relatedCollection}`;
162
+ }
163
+ return '';
164
+ }
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Command, Option } from 'commander';
2
+ import { version } from 'directus/version';
2
3
  import emitter from '../emitter.js';
3
4
  import { startServer } from '../server.js';
4
- import * as pkg from '../utils/package.js';
5
5
  import bootstrap from './commands/bootstrap/index.js';
6
6
  import count from './commands/count/index.js';
7
7
  import dbInstall from './commands/database/install.js';
@@ -20,7 +20,7 @@ export async function createCli() {
20
20
  await loadExtensions();
21
21
  await emitter.emitInit('cli.before', { program });
22
22
  program.name('directus').usage('[command] [options]');
23
- program.version(pkg.version, '-v, --version');
23
+ program.version(version, '-v, --version');
24
24
  program.command('start').description('Start the Directus API').action(startServer);
25
25
  program.command('init').description('Create a new Directus Project').action(init);
26
26
  // Security
@@ -1,7 +1,8 @@
1
1
  import type { SchemaInspector } from '@directus/schema';
2
2
  import type { Knex } from 'knex';
3
3
  import type { DatabaseClient } from '../types/index.js';
4
- export default function getDatabase(): Knex;
4
+ export default getDatabase;
5
+ export declare function getDatabase(): Knex;
5
6
  export declare function getSchemaInspector(): SchemaInspector;
6
7
  /**
7
8
  * Get database version. Value currently exists for MySQL only.
@@ -17,7 +17,8 @@ let database = null;
17
17
  let inspector = null;
18
18
  let databaseVersion = null;
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
- export default function getDatabase() {
20
+ export default getDatabase;
21
+ export function getDatabase() {
21
22
  if (database) {
22
23
  return database;
23
24
  }
@@ -120,6 +120,8 @@ fields:
120
120
  - field: theme_light_overrides
121
121
  width: full
122
122
  interface: system-theme-overrides
123
+ options:
124
+ appearance: light
123
125
  group: theming_group
124
126
  special:
125
127
  - cast-json
@@ -134,6 +136,8 @@ fields:
134
136
  - field: theme_dark_overrides
135
137
  width: full
136
138
  interface: system-theme-overrides
139
+ options:
140
+ appearance: dark
137
141
  group: theming_group
138
142
  special:
139
143
  - cast-json
@@ -124,6 +124,8 @@ fields:
124
124
  - field: theme_light_overrides
125
125
  width: full
126
126
  interface: system-theme-overrides
127
+ options:
128
+ appearance: light
127
129
  special:
128
130
  - cast-json
129
131
 
@@ -137,6 +139,8 @@ fields:
137
139
  - field: theme_dark_overrides
138
140
  width: full
139
141
  interface: system-theme-overrides
142
+ options:
143
+ appearance: dark
140
144
  special:
141
145
  - cast-json
142
146
 
@@ -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
@@ -17,4 +17,5 @@ export declare class Emitter {
17
17
  offAll(): void;
18
18
  }
19
19
  declare const emitter: Emitter;
20
+ export declare const useEmitter: () => Emitter;
20
21
  export default emitter;
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 logger from './logger.js';
4
4
  export class Emitter {
5
5
  filterEmitter;
6
6
  actionEmitter;
@@ -84,4 +84,5 @@ export class Emitter {
84
84
  }
85
85
  }
86
86
  const emitter = new Emitter();
87
+ export const useEmitter = () => emitter;
87
88
  export default emitter;
package/dist/env.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  export declare const defaults: Record<string, any>;
6
6
  declare let env: Record<string, any>;
7
7
  export default env;
8
+ export declare const useEnv: () => Record<string, any>;
8
9
  /**
9
10
  * When changes have been made during runtime, like in the CLI, we can refresh the env object with
10
11
  * the newly created variables
package/dist/env.js CHANGED
@@ -160,6 +160,7 @@ const allowedEnvironmentVars = [
160
160
  'PACKAGE_FILE_LOCATION',
161
161
  'EXTENSIONS_LOCATION',
162
162
  'EXTENSIONS_PATH',
163
+ 'EXTENSIONS_MUST_LOAD',
163
164
  'EXTENSIONS_AUTO_RELOAD',
164
165
  'EXTENSIONS_CACHE_TTL',
165
166
  'EXTENSIONS_SANDBOX_MEMORY',
@@ -196,6 +197,8 @@ const allowedEnvironmentVars = [
196
197
  'ADMIN_PASSWORD',
197
198
  // telemetry
198
199
  'TELEMETRY',
200
+ 'TELEMETRY_URL',
201
+ 'TELEMETRY_AUTHORIZATION',
199
202
  // limits & optimization
200
203
  'RELATIONAL_BATCH_SIZE',
201
204
  'EXPORT_BATCH_SIZE',
@@ -260,6 +263,7 @@ export const defaults = {
260
263
  AUTH_DISABLE_DEFAULT: false,
261
264
  PACKAGE_FILE_LOCATION: '.',
262
265
  EXTENSIONS_PATH: './extensions',
266
+ EXTENSIONS_MUST_LOAD: false,
263
267
  EXTENSIONS_AUTO_RELOAD: false,
264
268
  EXTENSIONS_SANDBOX_MEMORY: 100,
265
269
  EXTENSIONS_SANDBOX_TIMEOUT: 1000,
@@ -269,6 +273,7 @@ export const defaults = {
269
273
  EMAIL_SENDMAIL_NEW_LINE: 'unix',
270
274
  EMAIL_SENDMAIL_PATH: '/usr/sbin/sendmail',
271
275
  TELEMETRY: true,
276
+ TELEMETRY_URL: 'https://telemetry.directus.io',
272
277
  ASSETS_CACHE_TTL: '30d',
273
278
  ASSETS_TRANSFORM_MAX_CONCURRENT: 25,
274
279
  ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION: 6000,
@@ -336,6 +341,7 @@ let env = {
336
341
  process.env = env;
337
342
  env = processValues(env);
338
343
  export default env;
344
+ export const useEnv = () => env;
339
345
  /**
340
346
  * When changes have been made during runtime, like in the CLI, we can refresh the env object with
341
347
  * the newly created variables
@@ -12,7 +12,7 @@ export const getSharedDepsMapping = async (deps) => {
12
12
  const appDir = await readdir(path.join(resolvePackage('@directus/app', __dirname), 'dist', 'assets'));
13
13
  const depsMapping = {};
14
14
  for (const dep of deps) {
15
- const depRegex = new RegExp(`${escapeRegExp(dep.replace(/\//g, '_'))}\\.[0-9a-f]{8}\\.entry\\.js`);
15
+ const depRegex = new RegExp(`${escapeRegExp(dep.replace(/\//g, '_'))}\\.[a-zA-Z0-9_-]{8}\\.entry\\.js`);
16
16
  const depName = appDir.find((file) => depRegex.test(file));
17
17
  if (depName) {
18
18
  const depUrl = new Url(env['PUBLIC_URL']).addPath('admin', 'assets', depName);
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  import type { Router } from 'express';
2
3
  import type { Reference } from 'isolated-vm';
3
4
  import type { IncomingHttpHeaders } from 'node:http';
@@ -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
  }
@@ -27,6 +27,7 @@ import { getSchema } from '../utils/get-schema.js';
27
27
  import { importFileUrl } from '../utils/import-file-url.js';
28
28
  import { JobQueue } from '../utils/job-queue.js';
29
29
  import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
30
+ import { toBoolean } from '../utils/to-boolean.js';
30
31
  import { getExtensionsPath } from './lib/get-extensions-path.js';
31
32
  import { getExtensionsSettings } from './lib/get-extensions-settings.js';
32
33
  import { getExtensions } from './lib/get-extensions.js';
@@ -146,8 +147,7 @@ export class ExtensionManager {
146
147
  this.extensionsSettings = await getExtensionsSettings(this.extensions);
147
148
  }
148
149
  catch (error) {
149
- logger.warn(`Couldn't load extensions`);
150
- logger.warn(error);
150
+ this.handleExtensionError({ error, reason: `Couldn't load extensions` });
151
151
  }
152
152
  await this.registerHooks();
153
153
  await this.registerEndpoints();
@@ -292,7 +292,7 @@ export class ExtensionManager {
292
292
  find: name,
293
293
  replacement: path,
294
294
  }));
295
- const entrypoint = generateExtensionsEntrypoint(this.extensions);
295
+ const entrypoint = generateExtensionsEntrypoint(this.extensions, this.extensionsSettings);
296
296
  try {
297
297
  const bundle = await rollup({
298
298
  input: 'entry',
@@ -322,9 +322,9 @@ export class ExtensionManager {
322
322
  const extensionCode = await readFile(entrypointPath, 'utf-8');
323
323
  const isolate = new ivm.Isolate({
324
324
  memoryLimit: sandboxMemory,
325
- onCatastrophicError: (e) => {
325
+ onCatastrophicError: (error) => {
326
326
  logger.error(`Error in API extension sandbox of ${extension.type} "${extension.name}"`);
327
- logger.error(e);
327
+ logger.error(error);
328
328
  process.abort();
329
329
  },
330
330
  });
@@ -377,8 +377,7 @@ export class ExtensionManager {
377
377
  }
378
378
  }
379
379
  catch (error) {
380
- logger.warn(`Couldn't register hook "${hook.name}"`);
381
- logger.warn(error);
380
+ this.handleExtensionError({ error, reason: `Couldn't register hook "${hook.name}"` });
382
381
  }
383
382
  }
384
383
  }
@@ -410,8 +409,7 @@ export class ExtensionManager {
410
409
  }
411
410
  }
412
411
  catch (error) {
413
- logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
414
- logger.warn(error);
412
+ this.handleExtensionError({ error, reason: `Couldn't register endpoint "${endpoint.name}"` });
415
413
  }
416
414
  }
417
415
  }
@@ -449,8 +447,7 @@ export class ExtensionManager {
449
447
  }
450
448
  }
451
449
  catch (error) {
452
- logger.warn(`Couldn't register operation "${operation.name}"`);
453
- logger.warn(error);
450
+ this.handleExtensionError({ error, reason: `Couldn't register operation "${operation.name}"` });
454
451
  }
455
452
  }
456
453
  }
@@ -460,6 +457,12 @@ export class ExtensionManager {
460
457
  */
461
458
  async registerBundles() {
462
459
  const bundles = this.extensions.filter((extension) => extension.type === 'bundle');
460
+ const extensionEnabled = (extensionName) => {
461
+ const settings = this.extensionsSettings.find(({ name }) => name === extensionName);
462
+ if (!settings)
463
+ return false;
464
+ return settings.enabled;
465
+ };
463
466
  for (const bundle of bundles) {
464
467
  try {
465
468
  const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
@@ -469,14 +472,20 @@ export class ExtensionManager {
469
472
  const configs = getModuleDefault(bundleInstances);
470
473
  const unregisterFunctions = [];
471
474
  for (const { config, name } of configs.hooks) {
475
+ if (!extensionEnabled(`${bundle.name}/${name}`))
476
+ continue;
472
477
  const unregisters = this.registerHook(config, name);
473
478
  unregisterFunctions.push(...unregisters);
474
479
  }
475
480
  for (const { config, name } of configs.endpoints) {
481
+ if (!extensionEnabled(`${bundle.name}/${name}`))
482
+ continue;
476
483
  const unregister = this.registerEndpoint(config, name);
477
484
  unregisterFunctions.push(unregister);
478
485
  }
479
- for (const { config } of configs.operations) {
486
+ for (const { config, name } of configs.operations) {
487
+ if (!extensionEnabled(`${bundle.name}/${name}`))
488
+ continue;
480
489
  const unregister = this.registerOperation(config);
481
490
  unregisterFunctions.push(unregister);
482
491
  }
@@ -486,8 +495,7 @@ export class ExtensionManager {
486
495
  });
487
496
  }
488
497
  catch (error) {
489
- logger.warn(`Couldn't register bundle "${bundle.name}"`);
490
- logger.warn(error);
498
+ this.handleExtensionError({ error, reason: `Couldn't register bundle "${bundle.name}"` });
491
499
  }
492
500
  }
493
501
  }
@@ -534,7 +542,7 @@ export class ExtensionManager {
534
542
  });
535
543
  }
536
544
  else {
537
- logger.warn(`Couldn't register cron hook. Provided cron is invalid: ${cron}`);
545
+ this.handleExtensionError({ reason: `Couldn't register cron hook. Provided cron is invalid: ${cron}` });
538
546
  }
539
547
  },
540
548
  embed: (position, code) => {
@@ -556,7 +564,7 @@ export class ExtensionManager {
556
564
  }
557
565
  }
558
566
  else {
559
- logger.warn(`Couldn't register embed hook. Provided code is empty!`);
567
+ this.handleExtensionError({ reason: `Couldn't register embed hook. Provided code is empty!` });
560
568
  }
561
569
  },
562
570
  };
@@ -610,4 +618,22 @@ export class ExtensionManager {
610
618
  const unregisterFunctions = Array.from(this.unregisterFunctionMap.values());
611
619
  await Promise.all(unregisterFunctions.map((fn) => fn()));
612
620
  }
621
+ /**
622
+ * If extensions must load successfully, any errors will cause the process to exit.
623
+ * Otherwise, the error will only be logged as a warning.
624
+ */
625
+ handleExtensionError({ error, reason }) {
626
+ if (toBoolean(env['EXTENSIONS_MUST_LOAD'])) {
627
+ logger.error('EXTENSION_MUST_LOAD is enabled and an extension failed to load.');
628
+ logger.error(reason);
629
+ if (error)
630
+ logger.error(error);
631
+ process.exit(1);
632
+ }
633
+ else {
634
+ logger.warn(reason);
635
+ if (error)
636
+ logger.warn(error);
637
+ }
638
+ }
613
639
  }
package/dist/logger.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  import type { RequestHandler } from 'express';
3
3
  import { type LoggerOptions } from 'pino';
4
4
  export declare const httpLoggerOptions: LoggerOptions;
5
- declare const logger: import("pino").Logger<LoggerOptions & Record<string, any>>;
5
+ declare const logger: import("pino").Logger<never>;
6
6
  export declare const httpLoggerEnvConfig: Record<string, any>;
7
7
  export declare const expressLogger: RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
8
+ export declare const useLogger: () => import("pino").Logger<never>;
8
9
  export default logger;
package/dist/logger.js CHANGED
@@ -105,6 +105,7 @@ export const expressLogger = pinoHttp({
105
105
  },
106
106
  },
107
107
  });
108
+ export const useLogger = () => logger;
108
109
  export default logger;
109
110
  function redactQuery(originalPath) {
110
111
  const url = new URL(originalPath, 'http://example.com/');
@@ -10,17 +10,20 @@ if (env['RATE_LIMITER_ENABLED'] === true) {
10
10
  validateEnv(['RATE_LIMITER_STORE', 'RATE_LIMITER_DURATION', 'RATE_LIMITER_POINTS']);
11
11
  rateLimiter = createRateLimiter('RATE_LIMITER');
12
12
  checkRateLimit = asyncHandler(async (req, res, next) => {
13
- try {
14
- await rateLimiter.consume(getIPFromReq(req), 1);
15
- }
16
- catch (rateLimiterRes) {
17
- if (rateLimiterRes instanceof Error)
18
- throw rateLimiterRes;
19
- res.set('Retry-After', String(Math.round(rateLimiterRes.msBeforeNext / 1000)));
20
- throw new HitRateLimitError({
21
- limit: +env['RATE_LIMITER_POINTS'],
22
- reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
23
- });
13
+ const ip = getIPFromReq(req);
14
+ if (ip) {
15
+ try {
16
+ await rateLimiter.consume(ip, 1);
17
+ }
18
+ catch (rateLimiterRes) {
19
+ if (rateLimiterRes instanceof Error)
20
+ throw rateLimiterRes;
21
+ res.set('Retry-After', String(Math.round(rateLimiterRes.msBeforeNext / 1000)));
22
+ throw new HitRateLimitError({
23
+ limit: +env['RATE_LIMITER_POINTS'],
24
+ reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
25
+ });
26
+ }
24
27
  }
25
28
  next();
26
29
  });
@@ -0,0 +1,7 @@
1
+ import { Redis } from 'ioredis';
2
+ /**
3
+ * Create a new Redis instance based on the global env configuration
4
+ *
5
+ * @returns New Redis instance based on global configuration
6
+ */
7
+ export declare const createRedis: () => Redis;
@@ -0,0 +1,12 @@
1
+ import { Redis } from 'ioredis';
2
+ import { useEnv } from '../env.js';
3
+ import { getConfigFromEnv } from '../utils/get-config-from-env.js';
4
+ /**
5
+ * Create a new Redis instance based on the global env configuration
6
+ *
7
+ * @returns New Redis instance based on global configuration
8
+ */
9
+ export const createRedis = () => {
10
+ const env = useEnv();
11
+ return new Redis(env['REDIS'] ?? getConfigFromEnv('REDIS'));
12
+ };
@@ -0,0 +1,2 @@
1
+ export { useRedis } from './use-redis.js';
2
+ export { createRedis } from './create-redis.js';
@@ -0,0 +1,2 @@
1
+ export { useRedis } from './use-redis.js';
2
+ export { createRedis } from './create-redis.js';