@directus/api 22.1.1 → 22.2.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 (72) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/cache.d.ts +2 -2
  3. package/dist/cache.js +2 -2
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.js +1 -0
  6. package/dist/database/helpers/index.d.ts +2 -0
  7. package/dist/database/helpers/index.js +2 -0
  8. package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
  9. package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
  10. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
  11. package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
  12. package/dist/database/helpers/nullable-update/index.d.ts +7 -0
  13. package/dist/database/helpers/nullable-update/index.js +7 -0
  14. package/dist/database/helpers/nullable-update/types.d.ts +7 -0
  15. package/dist/database/helpers/nullable-update/types.js +12 -0
  16. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
  17. package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
  18. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  19. package/dist/database/helpers/schema/dialects/mssql.js +20 -0
  20. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  21. package/dist/database/helpers/schema/dialects/mysql.js +33 -0
  22. package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
  23. package/dist/database/helpers/schema/dialects/oracle.js +21 -0
  24. package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
  25. package/dist/database/helpers/schema/dialects/postgres.js +23 -0
  26. package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
  27. package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
  28. package/dist/database/helpers/schema/types.d.ts +5 -0
  29. package/dist/database/helpers/schema/types.js +3 -0
  30. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
  31. package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
  32. package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
  33. package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
  34. package/dist/database/run-ast/lib/get-db-query.js +14 -8
  35. package/dist/extensions/manager.js +2 -2
  36. package/dist/logger/index.d.ts +6 -0
  37. package/dist/logger/index.js +79 -28
  38. package/dist/logger/logs-stream.d.ts +11 -0
  39. package/dist/logger/logs-stream.js +41 -0
  40. package/dist/middleware/respond.js +1 -0
  41. package/dist/request/is-denied-ip.js +7 -1
  42. package/dist/server.js +4 -2
  43. package/dist/services/fields.js +58 -20
  44. package/dist/services/mail/index.js +1 -5
  45. package/dist/services/notifications.d.ts +0 -4
  46. package/dist/services/notifications.js +8 -6
  47. package/dist/services/server.js +8 -1
  48. package/dist/services/specifications.js +7 -7
  49. package/dist/services/users.js +4 -1
  50. package/dist/utils/get-address.d.ts +1 -1
  51. package/dist/utils/get-address.js +6 -1
  52. package/dist/utils/get-allowed-log-levels.d.ts +3 -0
  53. package/dist/utils/get-allowed-log-levels.js +11 -0
  54. package/dist/utils/get-schema.js +19 -24
  55. package/dist/utils/parse-filter-key.js +1 -5
  56. package/dist/utils/sanitize-schema.d.ts +1 -1
  57. package/dist/websocket/controllers/base.d.ts +10 -10
  58. package/dist/websocket/controllers/base.js +22 -3
  59. package/dist/websocket/controllers/graphql.js +3 -1
  60. package/dist/websocket/controllers/index.d.ts +4 -0
  61. package/dist/websocket/controllers/index.js +12 -0
  62. package/dist/websocket/controllers/logs.d.ts +18 -0
  63. package/dist/websocket/controllers/logs.js +50 -0
  64. package/dist/websocket/controllers/rest.js +3 -1
  65. package/dist/websocket/handlers/index.d.ts +1 -0
  66. package/dist/websocket/handlers/index.js +21 -3
  67. package/dist/websocket/handlers/logs.d.ts +31 -0
  68. package/dist/websocket/handlers/logs.js +121 -0
  69. package/dist/websocket/messages.d.ts +26 -0
  70. package/dist/websocket/messages.js +9 -0
  71. package/dist/websocket/types.d.ts +7 -0
  72. package/package.json +25 -25
@@ -1,12 +1,15 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { REDACTED_TEXT, toArray } from '@directus/utils';
2
+ import { REDACTED_TEXT, toArray, toBoolean } from '@directus/utils';
3
3
  import { merge } from 'lodash-es';
4
4
  import { URL } from 'node:url';
5
5
  import { pino } from 'pino';
6
6
  import { pinoHttp, stdSerializers } from 'pino-http';
7
+ import { httpPrintFactory } from 'pino-http-print';
8
+ import { build as pinoPretty } from 'pino-pretty';
7
9
  import { getConfigFromEnv } from '../utils/get-config-from-env.js';
10
+ import { LogsStream } from './logs-stream.js';
8
11
  import { redactQuery } from './redact-query.js';
9
- export const _cache = { logger: undefined };
12
+ export const _cache = { logger: undefined, logsStream: undefined, httpLogsStream: undefined };
10
13
  export const useLogger = () => {
11
14
  if (_cache.logger) {
12
15
  return _cache.logger;
@@ -14,6 +17,23 @@ export const useLogger = () => {
14
17
  _cache.logger = createLogger();
15
18
  return _cache.logger;
16
19
  };
20
+ export const getLogsStream = (pretty) => {
21
+ if (_cache.logsStream) {
22
+ return _cache.logsStream;
23
+ }
24
+ _cache.logsStream = new LogsStream(pretty ? 'basic' : false);
25
+ return _cache.logsStream;
26
+ };
27
+ export const getHttpLogsStream = (pretty) => {
28
+ if (_cache.httpLogsStream) {
29
+ return _cache.httpLogsStream;
30
+ }
31
+ _cache.httpLogsStream = new LogsStream(pretty ? 'http' : false);
32
+ return _cache.httpLogsStream;
33
+ };
34
+ export const getLoggerLevelValue = (level) => {
35
+ return pino.levels.values[level] || pino.levels.values['info'];
36
+ };
17
37
  export const createLogger = () => {
18
38
  const env = useEnv();
19
39
  const pinoOptions = {
@@ -23,15 +43,6 @@ export const createLogger = () => {
23
43
  censor: REDACTED_TEXT,
24
44
  },
25
45
  };
26
- if (env['LOG_STYLE'] !== 'raw') {
27
- pinoOptions.transport = {
28
- target: 'pino-pretty',
29
- options: {
30
- ignore: 'hostname,pid',
31
- sync: true,
32
- },
33
- };
34
- }
35
46
  const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
36
47
  // Expose custom log levels into formatter function
37
48
  if (loggerEnvConfig['levels']) {
@@ -50,7 +61,33 @@ export const createLogger = () => {
50
61
  };
51
62
  delete loggerEnvConfig['levels'];
52
63
  }
53
- return pino(merge(pinoOptions, loggerEnvConfig));
64
+ const mergedOptions = merge(pinoOptions, loggerEnvConfig);
65
+ const streams = [];
66
+ // Console Logs
67
+ if (env['LOG_STYLE'] !== 'raw') {
68
+ streams.push({
69
+ level: mergedOptions.level,
70
+ stream: pinoPretty({
71
+ ignore: 'hostname,pid',
72
+ sync: true,
73
+ }),
74
+ });
75
+ }
76
+ else {
77
+ streams.push({ level: mergedOptions.level, stream: process.stdout });
78
+ }
79
+ // WebSocket Logs
80
+ if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
81
+ const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info';
82
+ if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedOptions.level)) {
83
+ mergedOptions.level = wsLevel;
84
+ }
85
+ streams.push({
86
+ level: wsLevel,
87
+ stream: getLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'),
88
+ });
89
+ }
90
+ return pino(mergedOptions, pino.multistream(streams));
54
91
  };
55
92
  export const createExpressLogger = () => {
56
93
  const env = useEnv();
@@ -63,21 +100,7 @@ export const createExpressLogger = () => {
63
100
  censor: REDACTED_TEXT,
64
101
  },
65
102
  };
66
- if (env['LOG_STYLE'] !== 'raw') {
67
- httpLoggerOptions.transport = {
68
- target: 'pino-http-print',
69
- options: {
70
- all: true,
71
- translateTime: 'SYS:HH:MM:ss',
72
- relativeUrl: true,
73
- prettyOptions: {
74
- ignore: 'hostname,pid',
75
- sync: true,
76
- },
77
- },
78
- };
79
- }
80
- if (env['LOG_STYLE'] === 'raw') {
103
+ if (env['LOG_STYLE'] === 'raw' || toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
81
104
  httpLoggerOptions.redact = {
82
105
  paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers', 'req.query.access_token'],
83
106
  censor: (value, pathParts) => {
@@ -120,8 +143,36 @@ export const createExpressLogger = () => {
120
143
  },
121
144
  };
122
145
  }
146
+ const mergedHttpOptions = merge(httpLoggerOptions, loggerEnvConfig);
147
+ const streams = [];
148
+ if (env['LOG_STYLE'] !== 'raw') {
149
+ const pinoHttpPretty = httpPrintFactory({
150
+ all: true,
151
+ translateTime: 'SYS:HH:MM:ss',
152
+ relativeUrl: true,
153
+ prettyOptions: {
154
+ ignore: 'hostname,pid',
155
+ sync: true,
156
+ },
157
+ });
158
+ streams.push({ level: mergedHttpOptions.level, stream: pinoHttpPretty(process.stdout) });
159
+ }
160
+ else {
161
+ streams.push({ level: mergedHttpOptions.level, stream: process.stdout });
162
+ }
163
+ // WebSocket Logs
164
+ if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
165
+ const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info';
166
+ if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedHttpOptions.level)) {
167
+ mergedHttpOptions.level = wsLevel;
168
+ }
169
+ streams.push({
170
+ level: wsLevel,
171
+ stream: getHttpLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'),
172
+ });
173
+ }
123
174
  return pinoHttp({
124
- logger: pino(merge(httpLoggerOptions, loggerEnvConfig)),
175
+ logger: pino(mergedHttpOptions, pino.multistream(streams)),
125
176
  ...httpLoggerEnvConfig,
126
177
  serializers: {
127
178
  req(request) {
@@ -0,0 +1,11 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { Bus } from '@directus/memory';
3
+ import { Writable } from 'stream';
4
+ type PrettyType = 'basic' | 'http' | false;
5
+ export declare class LogsStream extends Writable {
6
+ messenger: Bus;
7
+ pretty: PrettyType;
8
+ constructor(pretty: PrettyType);
9
+ _write(chunk: string, _encoding: string, callback: (error?: Error | null) => void): void;
10
+ }
11
+ export {};
@@ -0,0 +1,41 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { Writable } from 'stream';
3
+ import { useBus } from '../bus/index.js';
4
+ const nodeId = nanoid(8);
5
+ export class LogsStream extends Writable {
6
+ messenger;
7
+ pretty;
8
+ constructor(pretty) {
9
+ super({ objectMode: true });
10
+ this.messenger = useBus();
11
+ this.pretty = pretty;
12
+ }
13
+ _write(chunk, _encoding, callback) {
14
+ if (!this.pretty) {
15
+ // keeping this string interpolation for performance on RAW logs
16
+ this.messenger.publish('logs', `{"log":${chunk},"nodeId":"${nodeId}"}`);
17
+ return callback();
18
+ }
19
+ const log = JSON.parse(chunk);
20
+ if (this.pretty === 'http' && log.req?.method && log.req?.url && log.res?.statusCode && log.responseTime) {
21
+ this.messenger.publish('logs', JSON.stringify({
22
+ log: {
23
+ level: log['level'],
24
+ time: log['time'],
25
+ msg: `${log.req.method} ${log.req.url} ${log.res.statusCode} ${log.responseTime}ms`,
26
+ },
27
+ nodeId: nodeId,
28
+ }));
29
+ return callback();
30
+ }
31
+ this.messenger.publish('logs', JSON.stringify({
32
+ log: {
33
+ level: log['level'],
34
+ time: log['time'],
35
+ msg: log['msg'],
36
+ },
37
+ nodeId: nodeId,
38
+ }));
39
+ callback();
40
+ }
41
+ }
@@ -20,6 +20,7 @@ export const respond = asyncHandler(async (req, res) => {
20
20
  exceedsMaxSize = valueSize > maxSize;
21
21
  }
22
22
  if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
23
+ req.originalUrl?.startsWith('/auth') === false &&
23
24
  env['CACHE_ENABLED'] === true &&
24
25
  cache &&
25
26
  !req.sanitizedQuery.export &&
@@ -1,5 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import os from 'node:os';
3
+ import { matches } from 'ip-matching';
3
4
  import { useLogger } from '../logger/index.js';
4
5
  import { ipInNetworks } from '../utils/ip-in-networks.js';
5
6
  export function isDeniedIp(ip) {
@@ -24,8 +25,13 @@ export function isDeniedIp(ip) {
24
25
  if (!networkInfo)
25
26
  continue;
26
27
  for (const info of networkInfo) {
27
- if (info.address === ip)
28
+ if (info.internal && info.cidr) {
29
+ if (matches(ip, info.cidr))
30
+ return true;
31
+ }
32
+ else if (info.address === ip) {
28
33
  return true;
34
+ }
29
35
  }
30
36
  }
31
37
  }
package/dist/server.js CHANGED
@@ -14,7 +14,7 @@ import { useLogger } from './logger/index.js';
14
14
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
15
15
  import { getIPFromReq } from './utils/get-ip-from-req.js';
16
16
  import { getAddress } from './utils/get-address.js';
17
- import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
17
+ import { createLogsController, createSubscriptionController, createWebSocketController, getLogsController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
18
18
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
19
19
  export let SERVER_ONLINE = true;
20
20
  const env = useEnv();
@@ -77,6 +77,7 @@ export async function createServer() {
77
77
  if (toBoolean(env['WEBSOCKETS_ENABLED']) === true) {
78
78
  createSubscriptionController(server);
79
79
  createWebSocketController(server);
80
+ createLogsController(server);
80
81
  startWebSocketHandlers();
81
82
  }
82
83
  const terminusOptions = {
@@ -99,6 +100,7 @@ export async function createServer() {
99
100
  async function onSignal() {
100
101
  getSubscriptionController()?.terminate();
101
102
  getWebSocketController()?.terminate();
103
+ getLogsController()?.terminate();
102
104
  const database = getDatabase();
103
105
  await database.destroy();
104
106
  logger.info('Database connections destroyed');
@@ -126,7 +128,7 @@ export async function startServer() {
126
128
  else {
127
129
  listenOptions = {
128
130
  host,
129
- port: parseInt(port || '8055'),
131
+ port: parseInt(port),
130
132
  };
131
133
  }
132
134
  server
@@ -5,7 +5,7 @@ import { createInspector } from '@directus/schema';
5
5
  import { addFieldFlag, toArray } from '@directus/utils';
6
6
  import { isEqual, isNil, merge } from 'lodash-es';
7
7
  import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
8
- import { ALIAS_TYPES } from '../constants.js';
8
+ import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js';
9
9
  import { translateDatabaseError } from '../database/errors/translate.js';
10
10
  import { getHelpers } from '../database/helpers/index.js';
11
11
  import getDatabase, { getSchemaInspector } from '../database/index.js';
@@ -384,8 +384,16 @@ export class FieldsService {
384
384
  }
385
385
  if (hookAdjustedField.schema) {
386
386
  const existingColumn = await this.columnInfo(collection, hookAdjustedField.field);
387
- if (hookAdjustedField.schema?.is_nullable === true && existingColumn.is_primary_key) {
388
- throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
387
+ if (existingColumn.is_primary_key) {
388
+ if (hookAdjustedField.schema?.is_nullable === true) {
389
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
390
+ }
391
+ if (hookAdjustedField.schema?.is_unique === false) {
392
+ throw new InvalidPayloadError({ reason: 'Primary key must be unique' });
393
+ }
394
+ if (hookAdjustedField.schema?.is_indexed === true) {
395
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be indexed' });
396
+ }
389
397
  }
390
398
  // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
391
399
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
@@ -662,12 +670,12 @@ export class FieldsService {
662
670
  else {
663
671
  throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
664
672
  }
665
- const defaultValue = field.schema?.default_value !== undefined ? field.schema?.default_value : existing?.default_value;
666
- if (defaultValue) {
673
+ const setDefaultValue = (defaultValue) => {
667
674
  const newDefaultValueIsString = typeof defaultValue === 'string';
668
675
  const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
669
676
  const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
670
677
  const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp;
678
+ const newDefaultIsAFunction = newDefaultValueIsString && ALLOWED_DB_DEFAULT_FUNCTIONS.includes(defaultValue);
671
679
  const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')');
672
680
  if (newDefaultIsSetToCurrentTime) {
673
681
  column.defaultTo(this.knex.fn.now());
@@ -676,31 +684,61 @@ export class FieldsService {
676
684
  const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1];
677
685
  column.defaultTo(this.knex.fn.now(Number(precision)));
678
686
  }
687
+ else if (newDefaultIsAFunction) {
688
+ column.defaultTo(this.knex.raw(defaultValue));
689
+ }
679
690
  else {
680
691
  column.defaultTo(defaultValue);
681
692
  }
693
+ };
694
+ // for a new item, set the default value and nullable as provided without any further considerations
695
+ if (!existing) {
696
+ if (field.schema?.default_value !== undefined) {
697
+ setDefaultValue(field.schema.default_value);
698
+ }
699
+ if (field.schema?.is_nullable || field.schema?.is_nullable === undefined) {
700
+ column.nullable();
701
+ }
702
+ else {
703
+ column.notNullable();
704
+ }
682
705
  }
683
706
  else {
684
- column.defaultTo(null);
685
- }
686
- const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
687
- if (isNullable) {
688
- column.nullable();
689
- }
690
- else {
691
- column.notNullable();
707
+ // for an existing item: if nullable option changed, we have to provide the default values as well and actually vice versa
708
+ // see https://knexjs.org/guide/schema-builder.html#alter
709
+ // To overwrite a nullable option with the same value this is not possible for Oracle though, hence the DB helper
710
+ if (field.schema?.default_value !== undefined || field.schema?.is_nullable !== undefined) {
711
+ this.helpers.nullableUpdate.updateNullableValue(column, field, existing);
712
+ let defaultValue = null;
713
+ if (field.schema?.default_value !== undefined) {
714
+ defaultValue = field.schema.default_value;
715
+ }
716
+ else if (existing.default_value !== undefined) {
717
+ defaultValue = existing.default_value;
718
+ }
719
+ setDefaultValue(defaultValue);
720
+ }
692
721
  }
693
722
  if (field.schema?.is_primary_key) {
694
723
  column.primary().notNullable();
695
724
  }
696
- else if (field.schema?.is_unique === true) {
697
- if (!existing || existing.is_unique === false) {
698
- column.unique();
725
+ else if (!existing?.is_primary_key) {
726
+ // primary key will already have unique/index constraints
727
+ if (field.schema?.is_unique === true) {
728
+ if (!existing || existing.is_unique === false) {
729
+ column.unique();
730
+ }
699
731
  }
700
- }
701
- else if (field.schema?.is_unique === false) {
702
- if (existing && existing.is_unique === true) {
703
- table.dropUnique([field.field]);
732
+ else if (field.schema?.is_unique === false) {
733
+ if (existing && existing.is_unique === true) {
734
+ table.dropUnique([field.field]);
735
+ }
736
+ }
737
+ if (field.schema?.is_indexed === true && !existing?.is_indexed) {
738
+ column.index();
739
+ }
740
+ else if (field.schema?.is_indexed === false && existing?.is_indexed) {
741
+ table.dropIndex([field.field]);
704
742
  }
705
743
  }
706
744
  if (existing) {
@@ -36,11 +36,7 @@ export class MailService {
36
36
  }
37
37
  }
38
38
  async send(options) {
39
- const payload = await emitter.emitFilter(`email.send`, options, {
40
- database: getDatabase(),
41
- schema: null,
42
- accountability: null,
43
- });
39
+ const payload = await emitter.emitFilter(`email.send`, options, {});
44
40
  if (!payload)
45
41
  return null;
46
42
  const { template, ...emailOptions } = payload;
@@ -1,11 +1,7 @@
1
1
  import type { Notification, PrimaryKey } from '@directus/types';
2
2
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
3
  import { ItemsService } from './items.js';
4
- import { MailService } from './mail/index.js';
5
- import { UsersService } from './users.js';
6
4
  export declare class NotificationsService extends ItemsService {
7
- usersService: UsersService;
8
- mailService: MailService;
9
5
  constructor(options: AbstractServiceOptions);
10
6
  createOne(data: Partial<Notification>, opts?: MutationOptions): Promise<PrimaryKey>;
11
7
  sendEmail(data: Partial<Notification>): Promise<void>;
@@ -10,12 +10,8 @@ import { UsersService } from './users.js';
10
10
  const env = useEnv();
11
11
  const logger = useLogger();
12
12
  export class NotificationsService extends ItemsService {
13
- usersService;
14
- mailService;
15
13
  constructor(options) {
16
14
  super('directus_notifications', options);
17
- this.usersService = new UsersService({ schema: this.schema });
18
- this.mailService = new MailService({ schema: this.schema, accountability: this.accountability });
19
15
  }
20
16
  async createOne(data, opts) {
21
17
  const response = await super.createOne(data, opts);
@@ -24,7 +20,8 @@ export class NotificationsService extends ItemsService {
24
20
  }
25
21
  async sendEmail(data) {
26
22
  if (data.recipient) {
27
- const user = await this.usersService.readOne(data.recipient, {
23
+ const usersService = new UsersService({ schema: this.schema, knex: this.knex });
24
+ const user = await usersService.readOne(data.recipient, {
28
25
  fields: ['id', 'email', 'email_notifications', 'role'],
29
26
  });
30
27
  if (user['email'] && user['email_notifications'] === true) {
@@ -38,7 +35,12 @@ export class NotificationsService extends ItemsService {
38
35
  roles,
39
36
  ip: null,
40
37
  }, this.knex);
41
- this.mailService
38
+ const mailService = new MailService({
39
+ schema: this.schema,
40
+ knex: this.knex,
41
+ accountability: this.accountability,
42
+ });
43
+ mailService
42
44
  .send({
43
45
  template: {
44
46
  name: 'base',
@@ -5,6 +5,7 @@ import { merge } from 'lodash-es';
5
5
  import { Readable } from 'node:stream';
6
6
  import { performance } from 'perf_hooks';
7
7
  import { getCache } from '../cache.js';
8
+ import { RESUMABLE_UPLOADS } from '../constants.js';
8
9
  import getDatabase, { hasDatabaseConnection } from '../database/index.js';
9
10
  import { useLogger } from '../logger/index.js';
10
11
  import getMailer from '../mailer.js';
@@ -12,8 +13,8 @@ import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
12
13
  import { rateLimiter } from '../middleware/rate-limiter-ip.js';
13
14
  import { SERVER_ONLINE } from '../server.js';
14
15
  import { getStorage } from '../storage/index.js';
16
+ import { getAllowedLogLevels } from '../utils/get-allowed-log-levels.js';
15
17
  import { SettingsService } from './settings.js';
16
- import { RESUMABLE_UPLOADS } from '../constants.js';
17
18
  const env = useEnv();
18
19
  const logger = useLogger();
19
20
  export class ServerService {
@@ -95,6 +96,12 @@ export class ServerService {
95
96
  info['websocket'].heartbeat = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED'])
96
97
  ? env['WEBSOCKETS_HEARTBEAT_PERIOD']
97
98
  : false;
99
+ info['websocket'].logs =
100
+ toBoolean(env['WEBSOCKETS_LOGS_ENABLED']) && this.accountability.admin
101
+ ? {
102
+ allowedLogLevels: getAllowedLogLevels(env['WEBSOCKETS_LOGS_LEVEL'] || 'info'),
103
+ }
104
+ : false;
98
105
  }
99
106
  else {
100
107
  info['websocket'] = false;
@@ -37,20 +37,20 @@ class OASSpecsService {
37
37
  this.schema = options.schema;
38
38
  }
39
39
  async generate(host) {
40
- let schema = this.schema;
40
+ let schemaForSpec = this.schema;
41
41
  let permissions = [];
42
42
  if (this.accountability && this.accountability.admin !== true) {
43
43
  const allowedFields = await fetchAllowedFieldMap({
44
44
  accountability: this.accountability,
45
45
  action: 'read',
46
- }, { schema, knex: this.knex });
47
- schema = reduceSchema(schema, allowedFields);
48
- const policies = await fetchPolicies(this.accountability, { schema, knex: this.knex });
49
- permissions = await fetchPermissions({ action: 'read', policies, accountability: this.accountability }, { schema, knex: this.knex });
46
+ }, { schema: this.schema, knex: this.knex });
47
+ schemaForSpec = reduceSchema(this.schema, allowedFields);
48
+ const policies = await fetchPolicies(this.accountability, { schema: this.schema, knex: this.knex });
49
+ permissions = await fetchPermissions({ policies, accountability: this.accountability }, { schema: this.schema, knex: this.knex });
50
50
  }
51
- const tags = await this.generateTags(schema);
51
+ const tags = await this.generateTags(schemaForSpec);
52
52
  const paths = await this.generatePaths(permissions, tags);
53
- const components = await this.generateComponents(schema, tags);
53
+ const components = await this.generateComponents(schemaForSpec, tags);
54
54
  const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
55
55
  const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
56
56
  const spec = {
@@ -102,7 +102,10 @@ export class UsersService extends ItemsService {
102
102
  */
103
103
  inviteUrl(email, url) {
104
104
  const payload = { email, scope: 'invite' };
105
- const token = jwt.sign(payload, getSecret(), { expiresIn: '7d', issuer: 'directus' });
105
+ const token = jwt.sign(payload, getSecret(), {
106
+ expiresIn: env['USER_INVITE_TOKEN_TTL'],
107
+ issuer: 'directus',
108
+ });
106
109
  return (url ? new Url(url) : new Url(env['PUBLIC_URL']).addPath('admin', 'accept-invite'))
107
110
  .setQuery('token', token)
108
111
  .toString();
@@ -2,4 +2,4 @@
2
2
  /// <reference types="node/http.js" />
3
3
  /// <reference types="pino-http" />
4
4
  import * as http from 'http';
5
- export declare function getAddress(server: http.Server): string | undefined;
5
+ export declare function getAddress(server: http.Server): {};
@@ -1,9 +1,14 @@
1
1
  import * as http from 'http';
2
+ import { useEnv } from '@directus/env';
2
3
  export function getAddress(server) {
4
+ const env = useEnv();
3
5
  const address = server.address();
4
6
  if (address === null) {
5
7
  // Before the 'listening' event has been emitted or after calling server.close()
6
- return;
8
+ if (env['UNIX_SOCKET_PATH']) {
9
+ return env['UNIX_SOCKET_PATH'];
10
+ }
11
+ return `${env['HOST']}:${env['PORT']}`;
7
12
  }
8
13
  if (typeof address === 'string') {
9
14
  // unix path
@@ -0,0 +1,3 @@
1
+ export declare const getAllowedLogLevels: (level: string) => {
2
+ [k: string]: number;
3
+ };
@@ -0,0 +1,11 @@
1
+ import { useLogger } from '../logger/index.js';
2
+ const logger = useLogger();
3
+ export const getAllowedLogLevels = (level) => {
4
+ const levelValue = logger.levels.values[level];
5
+ if (levelValue === undefined) {
6
+ throw new Error(`Invalid "${level}" log level`);
7
+ }
8
+ return Object.fromEntries(Object.entries(logger.levels.values)
9
+ .filter(([_, value]) => value >= levelValue)
10
+ .sort((a, b) => a[1] - b[1]));
11
+ };
@@ -4,7 +4,7 @@ import { systemCollectionRows } from '@directus/system-data';
4
4
  import { parseJSON, toArray } from '@directus/utils';
5
5
  import { mapValues } from 'lodash-es';
6
6
  import { useBus } from '../bus/index.js';
7
- import { getSchemaCache, setSchemaCache } from '../cache.js';
7
+ import { getLocalSchemaCache, setLocalSchemaCache } from '../cache.js';
8
8
  import { ALIAS_TYPES } from '../constants.js';
9
9
  import getDatabase from '../database/index.js';
10
10
  import { useLock } from '../lock/index.js';
@@ -22,7 +22,7 @@ export async function getSchema(options, attempt = 0) {
22
22
  const schemaInspector = createInspector(database);
23
23
  return await getDatabaseSchema(database, schemaInspector);
24
24
  }
25
- const cached = await getSchemaCache();
25
+ const cached = await getLocalSchemaCache();
26
26
  if (cached) {
27
27
  return cached;
28
28
  }
@@ -40,39 +40,34 @@ export async function getSchema(options, attempt = 0) {
40
40
  const currentProcessShouldHandleOperation = processId === 1;
41
41
  if (currentProcessShouldHandleOperation === false) {
42
42
  logger.trace('Schema cache is prepared in another process, waiting for result.');
43
- return new Promise((resolve, reject) => {
44
- const TIMEOUT = 10000;
45
- const timeout = setTimeout(() => {
46
- logger.trace('Did not receive schema callback message in time. Pulling schema...');
47
- callback().catch(reject);
48
- }, TIMEOUT);
49
- bus.subscribe(messageKey, callback);
50
- async function callback() {
51
- try {
52
- if (timeout)
53
- clearTimeout(timeout);
54
- const schema = await getSchema(options, attempt + 1);
55
- resolve(schema);
56
- }
57
- catch (error) {
58
- reject(error);
59
- }
60
- finally {
61
- bus.unsubscribe(messageKey, callback);
43
+ const timeout = new Promise((_, reject) => setTimeout(reject, env['CACHE_SCHEMA_SYNC_TIMEOUT']));
44
+ const subscription = new Promise((resolve, reject) => {
45
+ bus.subscribe(messageKey, busListener).catch(reject);
46
+ function busListener(options) {
47
+ if (options.schema === null) {
48
+ return reject();
62
49
  }
50
+ cleanup();
51
+ setLocalSchemaCache(options.schema).catch(reject);
52
+ resolve(options.schema);
53
+ }
54
+ function cleanup() {
55
+ bus.unsubscribe(messageKey, busListener).catch(reject);
63
56
  }
64
57
  });
58
+ return Promise.race([timeout, subscription]).catch(() => getSchema(options, attempt + 1));
65
59
  }
60
+ let schema = null;
66
61
  try {
67
62
  const database = options?.database || getDatabase();
68
63
  const schemaInspector = createInspector(database);
69
- const schema = await getDatabaseSchema(database, schemaInspector);
70
- await setSchemaCache(schema);
64
+ schema = await getDatabaseSchema(database, schemaInspector);
65
+ await setLocalSchemaCache(schema);
71
66
  return schema;
72
67
  }
73
68
  finally {
69
+ await bus.publish(messageKey, { schema });
74
70
  await lock.delete(lockKey);
75
- bus.publish(messageKey, { ready: true });
76
71
  }
77
72
  }
78
73
  async function getDatabaseSchema(database, schemaInspector) {
@@ -13,10 +13,6 @@ export function parseFilterKey(key) {
13
13
  const match = key.match(FILTER_KEY_REGEX);
14
14
  const fieldNameWithFunction = match?.[3]?.trim();
15
15
  const fieldName = fieldNameWithFunction || key.trim();
16
- let functionName;
17
- if (fieldNameWithFunction) {
18
- functionName = match?.[1]?.trim();
19
- return { fieldName, functionName };
20
- }
16
+ const functionName = fieldNameWithFunction ? match?.[1]?.trim() : undefined;
21
17
  return { fieldName, functionName };
22
18
  }