@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.
- package/dist/app.js +1 -1
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +2 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
- package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
- package/dist/database/helpers/nullable-update/index.d.ts +7 -0
- package/dist/database/helpers/nullable-update/index.js +7 -0
- package/dist/database/helpers/nullable-update/types.d.ts +7 -0
- package/dist/database/helpers/nullable-update/types.js +12 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +20 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +33 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/oracle.js +21 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/postgres.js +23 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
- package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
- package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
- package/dist/database/run-ast/lib/get-db-query.js +14 -8
- package/dist/extensions/manager.js +2 -2
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +11 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/middleware/respond.js +1 -0
- package/dist/request/is-denied-ip.js +7 -1
- package/dist/server.js +4 -2
- package/dist/services/fields.js +58 -20
- package/dist/services/mail/index.js +1 -5
- package/dist/services/notifications.d.ts +0 -4
- package/dist/services/notifications.js +8 -6
- package/dist/services/server.js +8 -1
- package/dist/services/specifications.js +7 -7
- package/dist/services/users.js +4 -1
- package/dist/utils/get-address.d.ts +1 -1
- package/dist/utils/get-address.js +6 -1
- package/dist/utils/get-allowed-log-levels.d.ts +3 -0
- package/dist/utils/get-allowed-log-levels.js +11 -0
- package/dist/utils/get-schema.js +19 -24
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/controllers/base.d.ts +10 -10
- package/dist/websocket/controllers/base.js +22 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -0
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +18 -0
- package/dist/websocket/controllers/logs.js +50 -0
- package/dist/websocket/controllers/rest.js +3 -1
- package/dist/websocket/handlers/index.d.ts +1 -0
- package/dist/websocket/handlers/index.js +21 -3
- package/dist/websocket/handlers/logs.d.ts +31 -0
- package/dist/websocket/handlers/logs.js +121 -0
- package/dist/websocket/messages.d.ts +26 -0
- package/dist/websocket/messages.js +9 -0
- package/dist/websocket/types.d.ts +7 -0
- package/package.json +25 -25
package/dist/logger/index.js
CHANGED
|
@@ -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
|
-
|
|
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']
|
|
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(
|
|
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.
|
|
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
|
|
131
|
+
port: parseInt(port),
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
server
|
package/dist/services/fields.js
CHANGED
|
@@ -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 (
|
|
388
|
-
|
|
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
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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 (
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
package/dist/services/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
48
|
-
const policies = await fetchPolicies(this.accountability, { schema, knex: this.knex });
|
|
49
|
-
permissions = await fetchPermissions({
|
|
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(
|
|
51
|
+
const tags = await this.generateTags(schemaForSpec);
|
|
52
52
|
const paths = await this.generatePaths(permissions, tags);
|
|
53
|
-
const components = await this.generateComponents(
|
|
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 = {
|
package/dist/services/users.js
CHANGED
|
@@ -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(), {
|
|
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();
|
|
@@ -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
|
-
|
|
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,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
|
+
};
|
package/dist/utils/get-schema.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
70
|
-
await
|
|
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
|
-
|
|
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
|
}
|