@directus/api 14.0.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/cli/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { Command, Option } from 'commander';
2
- import { isInstalled } from '../database/index.js';
3
2
  import emitter from '../emitter.js';
4
- import { getExtensionManager } from '../extensions/index.js';
5
3
  import { startServer } from '../server.js';
6
4
  import * as pkg from '../utils/package.js';
7
5
  import bootstrap from './commands/bootstrap/index.js';
@@ -16,12 +14,10 @@ import keyGenerate from './commands/security/key.js';
16
14
  import secretGenerate from './commands/security/secret.js';
17
15
  import usersCreate from './commands/users/create.js';
18
16
  import usersPasswd from './commands/users/passwd.js';
17
+ import { loadExtensions } from './load-extensions.js';
19
18
  export async function createCli() {
20
19
  const program = new Command();
21
- if ((await isInstalled()) === true) {
22
- const extensionManager = getExtensionManager();
23
- await extensionManager.initialize({ schedule: false, watch: false });
24
- }
20
+ await loadExtensions();
25
21
  await emitter.emitInit('cli.before', { program });
26
22
  program.name('directus').usage('[command] [options]');
27
23
  program.version(pkg.version, '-v, --version');
@@ -0,0 +1 @@
1
+ export declare const loadExtensions: () => Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { isInstalled, validateMigrations } from '../database/index.js';
2
+ import { getEnv } from '../env.js';
3
+ import { getExtensionManager } from '../extensions/index.js';
4
+ import logger from '../logger.js';
5
+ export const loadExtensions = async () => {
6
+ const env = getEnv();
7
+ if (!('DB_CLIENT' in env))
8
+ return;
9
+ const installed = await isInstalled();
10
+ if (!installed)
11
+ return;
12
+ const migrationsValid = await validateMigrations();
13
+ if (!migrationsValid) {
14
+ logger.info('Skipping CLI extensions initialization due to outstanding migrations.');
15
+ return;
16
+ }
17
+ const extensionManager = getExtensionManager();
18
+ await extensionManager.initialize({ schedule: false, watch: false });
19
+ };
@@ -99,7 +99,11 @@
99
99
  - preferences_divider
100
100
  - avatar
101
101
  - language
102
- - theme
102
+ - appearance
103
+ - theme_light
104
+ - theme_dark
105
+ - theme_light_overrides
106
+ - theme_dark_overrides
103
107
  - tfa_secret
104
108
  - status
105
109
  - role
@@ -104,6 +104,8 @@ fields:
104
104
  interface: select-dropdown
105
105
  options:
106
106
  choices:
107
+ - value: null
108
+ text: $t:appearance_global
107
109
  - value: auto
108
110
  text: $t:appearance_auto
109
111
  - value: light
package/dist/emitter.d.ts CHANGED
@@ -8,10 +8,10 @@ export declare class Emitter {
8
8
  emitFilter<T>(event: string | string[], payload: T, meta: Record<string, any>, context?: EventContext | null): Promise<T>;
9
9
  emitAction(event: string | string[], meta: Record<string, any>, context?: EventContext | null): void;
10
10
  emitInit(event: string, meta: Record<string, any>): Promise<void>;
11
- onFilter(event: string, handler: FilterHandler): void;
11
+ onFilter<T = unknown>(event: string, handler: FilterHandler<T>): void;
12
12
  onAction(event: string, handler: ActionHandler): void;
13
13
  onInit(event: string, handler: InitHandler): void;
14
- offFilter(event: string, handler: FilterHandler): void;
14
+ offFilter<T = unknown>(event: string, handler: FilterHandler<T>): void;
15
15
  offAction(event: string, handler: ActionHandler): void;
16
16
  offInit(event: string, handler: InitHandler): void;
17
17
  offAll(): void;
@@ -38,11 +38,12 @@ const virtual = virtualDefault;
38
38
  const alias = aliasDefault;
39
39
  const nodeResolve = nodeResolveDefault;
40
40
  const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ const defaultOptions = {
42
+ schedule: true,
43
+ watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
44
+ };
41
45
  export class ExtensionManager {
42
- options = {
43
- schedule: true,
44
- watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
45
- };
46
+ options = defaultOptions;
46
47
  /**
47
48
  * Whether or not the extensions have been read from disk and registered into the system
48
49
  */
@@ -105,12 +106,10 @@ export class ExtensionManager {
105
106
  * @param {boolean} options.watch - Whether or not to watch the local extensions folder for changes
106
107
  */
107
108
  async initialize(options = {}) {
108
- if (options.schedule !== undefined) {
109
- this.options.schedule = options.schedule;
110
- }
111
- if (options.watch !== undefined) {
112
- this.options.watch = options.watch;
113
- }
109
+ this.options = {
110
+ ...defaultOptions,
111
+ ...options,
112
+ };
114
113
  const wasWatcherInitialized = this.watcher !== null;
115
114
  if (this.options.watch && !wasWatcherInitialized) {
116
115
  this.initializeWatcher();
@@ -19,6 +19,16 @@ export const respond = asyncHandler(async (req, res) => {
19
19
  const maxSize = parseBytesConfiguration(env['CACHE_VALUE_MAX_SIZE']);
20
20
  exceedsMaxSize = valueSize > maxSize;
21
21
  }
22
+ if (req.sanitizedQuery.version &&
23
+ req.collection &&
24
+ (req.singleton || req.params['pk']) &&
25
+ 'data' in res.locals['payload']) {
26
+ const versionsService = new VersionsService({ accountability: req.accountability ?? null, schema: req.schema });
27
+ const saves = await versionsService.getVersionSaves(req.sanitizedQuery.version, req.collection, req.params['pk']);
28
+ if (saves) {
29
+ assign(res.locals['payload'].data, ...saves);
30
+ }
31
+ }
22
32
  if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
23
33
  env['CACHE_ENABLED'] === true &&
24
34
  cache &&
@@ -41,16 +51,6 @@ export const respond = asyncHandler(async (req, res) => {
41
51
  res.setHeader('Cache-Control', 'no-cache');
42
52
  res.setHeader('Vary', 'Origin, Cache-Control');
43
53
  }
44
- if (req.sanitizedQuery.version &&
45
- req.collection &&
46
- (req.singleton || req.params['pk']) &&
47
- 'data' in res.locals['payload']) {
48
- const versionsService = new VersionsService({ accountability: req.accountability ?? null, schema: req.schema });
49
- const saves = await versionsService.getVersionSaves(req.sanitizedQuery.version, req.collection, req.params['pk']);
50
- if (saves) {
51
- assign(res.locals['payload'].data, ...saves);
52
- }
53
- }
54
54
  if (req.sanitizedQuery.export) {
55
55
  const exportService = new ExportService({ accountability: req.accountability ?? null, schema: req.schema });
56
56
  let filename = '';
package/dist/server.js CHANGED
@@ -10,9 +10,9 @@ import emitter from './emitter.js';
10
10
  import env from './env.js';
11
11
  import logger from './logger.js';
12
12
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
13
+ import { toBoolean } from './utils/to-boolean.js';
13
14
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
14
15
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
15
- import { toBoolean } from './utils/to-boolean.js';
16
16
  export let SERVER_ONLINE = true;
17
17
  export async function createServer() {
18
18
  const server = http.createServer(await createApp());
@@ -116,6 +116,7 @@ export async function startServer() {
116
116
  server
117
117
  .listen(port, host, () => {
118
118
  logger.info(`Server started at http://${host}:${port}`);
119
+ process.send?.('ready');
119
120
  emitter.emitAction('server.start', { server }, {
120
121
  database: getDatabase(),
121
122
  schema: null,
@@ -89,7 +89,22 @@ export class ExtensionsService {
89
89
  let bundleName = null;
90
90
  let name = meta.name;
91
91
  if (name.includes('/')) {
92
- [bundleName, name] = name.split('/');
92
+ const parts = name.split('/');
93
+ // NPM packages can have an optional organization scope in the format
94
+ // `@<org>/<package>`. This is limited to a single `/`.
95
+ //
96
+ // `foo` -> extension
97
+ // `foo/bar` -> bundle
98
+ // `@rijk/foo` -> extension
99
+ // `@rijk/foo/bar -> bundle
100
+ const hasOrg = parts.at(0).startsWith('@');
101
+ if (hasOrg && parts.length > 2) {
102
+ name = parts.pop();
103
+ bundleName = parts.join('/');
104
+ }
105
+ else if (hasOrg === false) {
106
+ [bundleName, name] = parts;
107
+ }
93
108
  }
94
109
  let schema;
95
110
  if (bundleName) {
@@ -3,8 +3,10 @@ import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors
3
3
  import Joi from 'joi';
4
4
  import { assign, pick } from 'lodash-es';
5
5
  import objectHash from 'object-hash';
6
+ import { getCache } from '../cache.js';
6
7
  import getDatabase from '../database/index.js';
7
8
  import emitter from '../emitter.js';
9
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
8
10
  import { ActivityService } from './activity.js';
9
11
  import { AuthorizationService } from './authorization.js';
10
12
  import { ItemsService } from './items.js';
@@ -188,6 +190,10 @@ export class VersionsService extends ItemsService {
188
190
  data: revisionDelta,
189
191
  delta: revisionDelta,
190
192
  });
193
+ const { cache } = getCache();
194
+ if (shouldClearCache(cache, undefined, version['collection'])) {
195
+ cache.clear();
196
+ }
191
197
  return data;
192
198
  }
193
199
  async promote(version, mainHash, fields) {
@@ -19,5 +19,5 @@ export declare function redactObject(input: UnknownObject, redact: {
19
19
  /**
20
20
  * Replace values and extract Error objects for use with JSON.stringify()
21
21
  */
22
- export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => unknown;
22
+ export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => any;
23
23
  export {};
@@ -84,31 +84,44 @@ export function getReplacer(replacement, values) {
84
84
  const filteredValues = values
85
85
  ? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
86
86
  : [];
87
- const seen = new WeakSet();
88
- return (_key, value) => {
89
- // Skip circular values
90
- if (isObject(value)) {
91
- if (seen.has(value)) {
92
- return;
87
+ const replacer = (seen) => {
88
+ return function (_key, value) {
89
+ if (value instanceof Error) {
90
+ return {
91
+ name: value.name,
92
+ message: value.message,
93
+ stack: value.stack,
94
+ cause: value.cause,
95
+ };
93
96
  }
94
- seen.add(value);
95
- }
96
- if (value instanceof Error) {
97
- return {
98
- name: value.name,
99
- message: value.message,
100
- stack: value.stack,
101
- cause: value.cause,
102
- };
103
- }
104
- if (!values || filteredValues.length === 0 || typeof value !== 'string')
105
- return value;
106
- let finalValue = value;
107
- for (const [redactKey, valueToRedact] of filteredValues) {
108
- if (finalValue.includes(valueToRedact)) {
109
- finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
97
+ if (value !== null && typeof value === 'object') {
98
+ if (seen.has(value)) {
99
+ return '[Circular]';
100
+ }
101
+ seen.add(value);
102
+ const newValue = Array.isArray(value) ? [] : {};
103
+ for (const [key2, value2] of Object.entries(value)) {
104
+ if (typeof value2 === 'string') {
105
+ newValue[key2] = value2;
106
+ }
107
+ else {
108
+ newValue[key2] = replacer(seen)(key2, value2);
109
+ }
110
+ }
111
+ seen.delete(value);
112
+ return newValue;
110
113
  }
111
- }
112
- return finalValue;
114
+ if (!values || filteredValues.length === 0 || typeof value !== 'string')
115
+ return value;
116
+ let finalValue = value;
117
+ for (const [redactKey, valueToRedact] of filteredValues) {
118
+ if (finalValue.includes(valueToRedact)) {
119
+ finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
120
+ }
121
+ }
122
+ return finalValue;
123
+ };
113
124
  };
125
+ const seen = new WeakSet();
126
+ return replacer(seen);
114
127
  }
@@ -1,3 +1,4 @@
1
+ import os from 'node:os';
1
2
  import Tinypool from 'tinypool';
2
3
  let workerPool;
3
4
  export function getWorkerPool() {
@@ -6,6 +7,13 @@ export function getWorkerPool() {
6
7
  minThreads: 0,
7
8
  maxQueue: 'auto',
8
9
  });
10
+ // TODO Workaround currently required for failing CPU count on ARM in Tinypool,
11
+ // remove again once fixed upstream
12
+ if (workerPool.options.maxThreads === 0) {
13
+ const availableParallelism = os.availableParallelism();
14
+ workerPool.options.maxThreads = availableParallelism;
15
+ workerPool.options.maxQueue = availableParallelism ** 2;
16
+ }
9
17
  }
10
18
  return workerPool;
11
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "14.0.0",
3
+ "version": "14.0.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -144,14 +144,14 @@
144
144
  "ws": "8.14.2",
145
145
  "zod": "3.22.4",
146
146
  "zod-validation-error": "1.0.1",
147
- "@directus/app": "10.10.0",
147
+ "@directus/app": "10.11.0",
148
148
  "@directus/constants": "11.0.1",
149
149
  "@directus/errors": "0.2.0",
150
- "@directus/extensions": "0.1.0",
151
- "@directus/extensions-sdk": "10.1.13",
150
+ "@directus/extensions": "0.1.1",
151
+ "@directus/extensions-sdk": "10.1.14",
152
152
  "@directus/pressure": "1.0.12",
153
153
  "@directus/schema": "11.0.0",
154
- "@directus/specs": "10.2.0",
154
+ "@directus/specs": "10.2.1",
155
155
  "@directus/storage": "10.0.7",
156
156
  "@directus/storage-driver-azure": "10.0.13",
157
157
  "@directus/storage-driver-cloudinary": "10.0.13",