@directus/api 24.0.1 → 25.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/app.js +10 -4
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +20 -7
- package/dist/controllers/assets.js +2 -2
- package/dist/controllers/metrics.d.ts +2 -0
- package/dist/controllers/metrics.js +33 -0
- package/dist/controllers/server.js +1 -1
- package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
- package/dist/database/helpers/number/dialects/mssql.js +3 -3
- package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
- package/dist/database/helpers/number/dialects/oracle.js +2 -2
- package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
- package/dist/database/helpers/number/dialects/sqlite.js +2 -2
- package/dist/database/helpers/number/types.d.ts +2 -2
- package/dist/database/helpers/number/types.js +2 -2
- package/dist/database/index.js +3 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/lib/create-metrics.d.ts +15 -0
- package/dist/metrics/lib/create-metrics.js +239 -0
- package/dist/metrics/lib/use-metrics.d.ts +17 -0
- package/dist/metrics/lib/use-metrics.js +15 -0
- package/dist/metrics/types/metric.d.ts +1 -0
- package/dist/metrics/types/metric.js +1 -0
- package/dist/middleware/respond.js +7 -1
- package/dist/operations/condition/index.js +7 -2
- package/dist/schedules/metrics.d.ts +7 -0
- package/dist/schedules/metrics.js +44 -0
- package/dist/services/assets.d.ts +6 -1
- package/dist/services/assets.js +8 -6
- package/dist/services/collections.js +31 -1
- package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
- package/dist/services/fields/build-collection-and-field-relations.js +55 -0
- package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
- package/dist/services/fields/get-collection-meta-updates.js +72 -0
- package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
- package/dist/services/fields/get-collection-relation-list.js +28 -0
- package/dist/services/fields.js +18 -13
- package/dist/services/graphql/errors/format.d.ts +6 -0
- package/dist/services/graphql/errors/format.js +14 -0
- package/dist/services/graphql/index.d.ts +5 -53
- package/dist/services/graphql/index.js +5 -2720
- package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
- package/dist/services/graphql/resolvers/mutation.js +74 -0
- package/dist/services/graphql/resolvers/query.d.ts +8 -0
- package/dist/services/graphql/resolvers/query.js +87 -0
- package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
- package/dist/services/graphql/resolvers/system-admin.js +236 -0
- package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
- package/dist/services/graphql/resolvers/system-global.js +435 -0
- package/dist/services/graphql/resolvers/system.d.ts +11 -0
- package/dist/services/graphql/resolvers/system.js +554 -0
- package/dist/services/graphql/schema/get-types.d.ts +12 -0
- package/dist/services/graphql/schema/get-types.js +223 -0
- package/dist/services/graphql/schema/index.d.ts +32 -0
- package/dist/services/graphql/schema/index.js +190 -0
- package/dist/services/graphql/schema/parse-args.d.ts +9 -0
- package/dist/services/graphql/schema/parse-args.js +35 -0
- package/dist/services/graphql/schema/parse-query.d.ts +7 -0
- package/dist/services/graphql/schema/parse-query.js +98 -0
- package/dist/services/graphql/schema/read.d.ts +12 -0
- package/dist/services/graphql/schema/read.js +653 -0
- package/dist/services/graphql/schema/write.d.ts +9 -0
- package/dist/services/graphql/schema/write.js +142 -0
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/subscription.js +7 -6
- package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
- package/dist/services/graphql/utils/aggrgate-query.js +32 -0
- package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
- package/dist/services/graphql/utils/replace-fragments.js +21 -0
- package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
- package/dist/services/graphql/utils/replace-funcs.js +21 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
- package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
- package/dist/services/items.js +0 -2
- package/dist/services/meta.js +27 -84
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +23 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +58 -21
- package/dist/utils/freeze-schema.d.ts +3 -0
- package/dist/utils/freeze-schema.js +31 -0
- package/dist/utils/get-accountability-for-token.js +1 -0
- package/dist/utils/get-milliseconds.js +1 -1
- package/dist/utils/get-schema.js +10 -5
- package/dist/utils/permissions-cachable.d.ts +8 -0
- package/dist/utils/permissions-cachable.js +38 -0
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/messages.d.ts +6 -6
- package/package.json +23 -20
package/dist/app.js
CHANGED
|
@@ -9,8 +9,8 @@ import { createRequire } from 'node:module';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import qs from 'qs';
|
|
11
11
|
import { registerAuthProviders } from './auth.js';
|
|
12
|
-
import activityRouter from './controllers/activity.js';
|
|
13
12
|
import accessRouter from './controllers/access.js';
|
|
13
|
+
import activityRouter from './controllers/activity.js';
|
|
14
14
|
import assetsRouter from './controllers/assets.js';
|
|
15
15
|
import authRouter from './controllers/auth.js';
|
|
16
16
|
import collectionsRouter from './controllers/collections.js';
|
|
@@ -23,6 +23,7 @@ import flowsRouter from './controllers/flows.js';
|
|
|
23
23
|
import foldersRouter from './controllers/folders.js';
|
|
24
24
|
import graphqlRouter from './controllers/graphql.js';
|
|
25
25
|
import itemsRouter from './controllers/items.js';
|
|
26
|
+
import metricsRouter from './controllers/metrics.js';
|
|
26
27
|
import notFoundHandler from './controllers/not-found.js';
|
|
27
28
|
import notificationsRouter from './controllers/notifications.js';
|
|
28
29
|
import operationsRouter from './controllers/operations.js';
|
|
@@ -43,9 +44,6 @@ import usersRouter from './controllers/users.js';
|
|
|
43
44
|
import utilsRouter from './controllers/utils.js';
|
|
44
45
|
import versionsRouter from './controllers/versions.js';
|
|
45
46
|
import webhooksRouter from './controllers/webhooks.js';
|
|
46
|
-
import retentionSchedule from './schedules/retention.js';
|
|
47
|
-
import telemetrySchedule from './schedules/telemetry.js';
|
|
48
|
-
import tusSchedule from './schedules/tus.js';
|
|
49
47
|
import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations, } from './database/index.js';
|
|
50
48
|
import emitter from './emitter.js';
|
|
51
49
|
import { getExtensionManager } from './extensions/index.js';
|
|
@@ -60,6 +58,10 @@ import rateLimiterGlobal from './middleware/rate-limiter-global.js';
|
|
|
60
58
|
import rateLimiter from './middleware/rate-limiter-ip.js';
|
|
61
59
|
import sanitizeQuery from './middleware/sanitize-query.js';
|
|
62
60
|
import schema from './middleware/schema.js';
|
|
61
|
+
import metricsSchedule from './schedules/metrics.js';
|
|
62
|
+
import retentionSchedule from './schedules/retention.js';
|
|
63
|
+
import telemetrySchedule from './schedules/telemetry.js';
|
|
64
|
+
import tusSchedule from './schedules/tus.js';
|
|
63
65
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
64
66
|
import { Url } from './utils/url.js';
|
|
65
67
|
import { validateStorage } from './utils/validate-storage.js';
|
|
@@ -223,6 +225,9 @@ export default async function createApp() {
|
|
|
223
225
|
app.use('/flows', flowsRouter);
|
|
224
226
|
app.use('/folders', foldersRouter);
|
|
225
227
|
app.use('/items', itemsRouter);
|
|
228
|
+
if (env['METRICS_ENABLED'] === true) {
|
|
229
|
+
app.use('/metrics', metricsRouter);
|
|
230
|
+
}
|
|
226
231
|
app.use('/notifications', notificationsRouter);
|
|
227
232
|
app.use('/operations', operationsRouter);
|
|
228
233
|
app.use('/panels', panelsRouter);
|
|
@@ -251,6 +256,7 @@ export default async function createApp() {
|
|
|
251
256
|
await retentionSchedule();
|
|
252
257
|
await telemetrySchedule();
|
|
253
258
|
await tusSchedule();
|
|
259
|
+
await metricsSchedule();
|
|
254
260
|
await emitter.emitInit('app.after', { app });
|
|
255
261
|
return app;
|
|
256
262
|
}
|
|
@@ -19,6 +19,7 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
|
19
19
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
20
20
|
import { getSecret } from '../../utils/get-secret.js';
|
|
21
21
|
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
|
|
22
|
+
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
23
|
import { Url } from '../../utils/url.js';
|
|
23
24
|
import { LocalAuthDriver } from './local.js';
|
|
24
25
|
export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
@@ -254,9 +255,7 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
254
255
|
const logger = useLogger();
|
|
255
256
|
let tokenData;
|
|
256
257
|
try {
|
|
257
|
-
tokenData =
|
|
258
|
-
issuer: 'directus',
|
|
259
|
-
});
|
|
258
|
+
tokenData = verifyJWT(req.cookies[`oauth2.${providerName}`], getSecret());
|
|
260
259
|
}
|
|
261
260
|
catch (e) {
|
|
262
261
|
logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
|
|
@@ -19,6 +19,7 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
|
19
19
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
20
20
|
import { getSecret } from '../../utils/get-secret.js';
|
|
21
21
|
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
|
|
22
|
+
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
23
|
import { Url } from '../../utils/url.js';
|
|
23
24
|
import { LocalAuthDriver } from './local.js';
|
|
24
25
|
export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
@@ -306,9 +307,7 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
306
307
|
const logger = useLogger();
|
|
307
308
|
let tokenData;
|
|
308
309
|
try {
|
|
309
|
-
tokenData =
|
|
310
|
-
issuer: 'directus',
|
|
311
|
-
});
|
|
310
|
+
tokenData = verifyJWT(req.cookies[`openid.${providerName}`], getSecret());
|
|
312
311
|
}
|
|
313
312
|
catch (e) {
|
|
314
313
|
logger.warn(e, `[OpenID] Couldn't verify OpenID cookie`);
|
package/dist/cache.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function clearSystemCache(opts?: {
|
|
|
13
13
|
}): Promise<void>;
|
|
14
14
|
export declare function setSystemCache(key: string, value: any, ttl?: number): Promise<void>;
|
|
15
15
|
export declare function getSystemCache(key: string): Promise<Record<string, any>>;
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
16
|
+
export declare function setMemorySchemaCache(schema: SchemaOverview): void;
|
|
17
|
+
export declare function getMemorySchemaCache(): Readonly<SchemaOverview> | undefined;
|
|
18
18
|
export declare function setCacheValue(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl?: number): Promise<void>;
|
|
19
19
|
export declare function getCacheValue(cache: Keyv, key: string): Promise<any>;
|
package/dist/cache.js
CHANGED
|
@@ -9,14 +9,16 @@ import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
|
9
9
|
import { getMilliseconds } from './utils/get-milliseconds.js';
|
|
10
10
|
import { validateEnv } from './utils/validate-env.js';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { freezeSchema, unfreezeSchema } from './utils/freeze-schema.js';
|
|
12
13
|
const logger = useLogger();
|
|
13
14
|
const env = useEnv();
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
let cache = null;
|
|
16
17
|
let systemCache = null;
|
|
17
|
-
let localSchemaCache = null;
|
|
18
18
|
let lockCache = null;
|
|
19
19
|
let messengerSubscribed = false;
|
|
20
|
+
let localSchemaCache = null;
|
|
21
|
+
let memorySchemaCache = null;
|
|
20
22
|
const messenger = useBus();
|
|
21
23
|
if (redisConfigAvailable() && !messengerSubscribed) {
|
|
22
24
|
messengerSubscribed = true;
|
|
@@ -25,6 +27,7 @@ if (redisConfigAvailable() && !messengerSubscribed) {
|
|
|
25
27
|
await cache.clear();
|
|
26
28
|
}
|
|
27
29
|
await localSchemaCache?.clear();
|
|
30
|
+
memorySchemaCache = null;
|
|
28
31
|
});
|
|
29
32
|
}
|
|
30
33
|
export function getCache() {
|
|
@@ -61,6 +64,7 @@ export async function clearSystemCache(opts) {
|
|
|
61
64
|
await lockCache.delete('system-cache-lock');
|
|
62
65
|
}
|
|
63
66
|
await localSchemaCache.clear();
|
|
67
|
+
memorySchemaCache = null;
|
|
64
68
|
// Since a lot of cached permission function rely on the schema it needs to be cleared as well
|
|
65
69
|
await clearPermissionCache();
|
|
66
70
|
messenger.publish('schemaChanged', { autoPurgeCache: opts?.autoPurgeCache });
|
|
@@ -75,13 +79,22 @@ export async function getSystemCache(key) {
|
|
|
75
79
|
const { systemCache } = getCache();
|
|
76
80
|
return await getCacheValue(systemCache, key);
|
|
77
81
|
}
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
export function setMemorySchemaCache(schema) {
|
|
83
|
+
if (Object.isFrozen(schema)) {
|
|
84
|
+
memorySchemaCache = schema;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
memorySchemaCache = freezeSchema(schema);
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
|
-
export
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
export function getMemorySchemaCache() {
|
|
91
|
+
if (env['CACHE_SCHEMA_FREEZE_ENABLED']) {
|
|
92
|
+
return memorySchemaCache ?? undefined;
|
|
93
|
+
}
|
|
94
|
+
else if (memorySchemaCache) {
|
|
95
|
+
return unfreezeSchema(memorySchemaCache);
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
85
98
|
}
|
|
86
99
|
export async function setCacheValue(cache, key, value, ttl) {
|
|
87
100
|
const compressed = await compress(value);
|
|
@@ -151,7 +151,7 @@ asyncHandler(async (req, res) => {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
const { stream, file, stat } = await service.getAsset(id, { transformationParams, acceptFormat }, range);
|
|
154
|
+
const { stream, file, stat } = await service.getAsset(id, { transformationParams, acceptFormat }, range, true);
|
|
155
155
|
const filename = req.params['filename'] ?? file.filename_download;
|
|
156
156
|
res.attachment(filename);
|
|
157
157
|
res.setHeader('Content-Type', file.type);
|
|
@@ -180,7 +180,7 @@ asyncHandler(async (req, res) => {
|
|
|
180
180
|
res.setHeader('Content-Length', stat.size);
|
|
181
181
|
return res.end();
|
|
182
182
|
}
|
|
183
|
-
stream
|
|
183
|
+
(await stream())
|
|
184
184
|
.on('error', (error) => {
|
|
185
185
|
logger.error(error, `Couldn't stream file ${file.id} to the client`);
|
|
186
186
|
if (!res.headersSent) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { ForbiddenError } from '@directus/errors';
|
|
3
|
+
import { Router } from 'express';
|
|
4
|
+
import { useMetrics } from '../metrics/index.js';
|
|
5
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
6
|
+
const env = useEnv();
|
|
7
|
+
const router = Router();
|
|
8
|
+
const metrics = useMetrics();
|
|
9
|
+
router.get('/', asyncHandler(async (req, _res, next) => {
|
|
10
|
+
if (req.accountability?.admin === true) {
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
13
|
+
// support Bearer Token of type `Metrics`
|
|
14
|
+
const metricTokens = env['METRICS_TOKENS'];
|
|
15
|
+
if (!req.headers || !req.headers.authorization || !metricTokens) {
|
|
16
|
+
throw new ForbiddenError();
|
|
17
|
+
}
|
|
18
|
+
const parts = req.headers.authorization.split(' ');
|
|
19
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== 'metrics') {
|
|
20
|
+
throw new ForbiddenError();
|
|
21
|
+
}
|
|
22
|
+
if (metricTokens.find((mt) => mt.toString() === parts[1]) !== undefined) {
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
throw new ForbiddenError();
|
|
26
|
+
}), asyncHandler(async (_req, res) => {
|
|
27
|
+
res.set('Content-Type', 'text/plain');
|
|
28
|
+
// Don't cache anything by default
|
|
29
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
30
|
+
res.setHeader('Vary', 'Origin, Cache-Control');
|
|
31
|
+
return res.send(await metrics?.readAll());
|
|
32
|
+
}));
|
|
33
|
+
export default router;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { RouteNotFoundError } from '@directus/errors';
|
|
1
2
|
import { format } from 'date-fns';
|
|
2
3
|
import { Router } from 'express';
|
|
3
|
-
import { RouteNotFoundError } from '@directus/errors';
|
|
4
4
|
import { respond } from '../middleware/respond.js';
|
|
5
5
|
import { ServerService } from '../services/server.js';
|
|
6
6
|
import { SpecificationService } from '../services/specifications.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { NumericValue } from '@directus/types';
|
|
1
2
|
import type { Knex } from 'knex';
|
|
2
3
|
import { NumberDatabaseHelper, type NumberInfo } from '../types.js';
|
|
3
|
-
import type { NumericValue } from '@directus/types';
|
|
4
4
|
export declare class NumberHelperMSSQL extends NumberDatabaseHelper {
|
|
5
|
-
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
|
|
5
|
+
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue, logical: 'and' | 'or'): Knex.QueryBuilder;
|
|
6
6
|
isNumberValid(value: NumericValue, info: NumberInfo): boolean;
|
|
7
7
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { NumberDatabaseHelper } from '../types.js';
|
|
1
2
|
import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
|
|
2
3
|
import { numberInRange } from '../utils/number-in-range.js';
|
|
3
|
-
import { NumberDatabaseHelper } from '../types.js';
|
|
4
4
|
export class NumberHelperMSSQL extends NumberDatabaseHelper {
|
|
5
|
-
addSearchCondition(dbQuery, collection, name, value) {
|
|
6
|
-
return dbQuery.
|
|
5
|
+
addSearchCondition(dbQuery, collection, name, value, logical) {
|
|
6
|
+
return dbQuery[logical].where({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
|
|
7
7
|
}
|
|
8
8
|
isNumberValid(value, info) {
|
|
9
9
|
return numberInRange(value, info);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { NumericValue } from '@directus/types';
|
|
1
2
|
import type { Knex } from 'knex';
|
|
2
3
|
import { NumberDatabaseHelper } from '../types.js';
|
|
3
|
-
import type { NumericValue } from '@directus/types';
|
|
4
4
|
export declare class NumberHelperOracle extends NumberDatabaseHelper {
|
|
5
|
-
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
|
|
5
|
+
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue, logical: 'and' | 'or'): Knex.QueryBuilder;
|
|
6
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NumberDatabaseHelper } from '../types.js';
|
|
2
2
|
import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
|
|
3
3
|
export class NumberHelperOracle extends NumberDatabaseHelper {
|
|
4
|
-
addSearchCondition(dbQuery, collection, name, value) {
|
|
5
|
-
return dbQuery.
|
|
4
|
+
addSearchCondition(dbQuery, collection, name, value, logical) {
|
|
5
|
+
return dbQuery[logical].where({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { NumericValue } from '@directus/types';
|
|
1
2
|
import type { Knex } from 'knex';
|
|
2
3
|
import { NumberDatabaseHelper } from '../types.js';
|
|
3
|
-
import type { NumericValue } from '@directus/types';
|
|
4
4
|
export declare class NumberHelperSQLite extends NumberDatabaseHelper {
|
|
5
|
-
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
|
|
5
|
+
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue, logical: 'and' | 'or'): Knex.QueryBuilder;
|
|
6
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NumberDatabaseHelper } from '../types.js';
|
|
2
2
|
import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
|
|
3
3
|
export class NumberHelperSQLite extends NumberDatabaseHelper {
|
|
4
|
-
addSearchCondition(dbQuery, collection, name, value) {
|
|
5
|
-
return dbQuery.
|
|
4
|
+
addSearchCondition(dbQuery, collection, name, value, logical) {
|
|
5
|
+
return dbQuery[logical].where({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import type { NumericType, NumericValue } from '@directus/types';
|
|
1
2
|
import type { Knex } from 'knex';
|
|
2
3
|
import { DatabaseHelper } from '../types.js';
|
|
3
|
-
import type { NumericType, NumericValue } from '@directus/types';
|
|
4
4
|
export type NumberInfo = {
|
|
5
5
|
type: NumericType;
|
|
6
6
|
precision: number | null;
|
|
7
7
|
scale: number | null;
|
|
8
8
|
};
|
|
9
9
|
export declare abstract class NumberDatabaseHelper extends DatabaseHelper {
|
|
10
|
-
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
|
|
10
|
+
addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue, logical: 'and' | 'or'): Knex.QueryBuilder;
|
|
11
11
|
isNumberValid(_value: NumericValue, _info: NumberInfo): boolean;
|
|
12
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DatabaseHelper } from '../types.js';
|
|
2
2
|
export class NumberDatabaseHelper extends DatabaseHelper {
|
|
3
|
-
addSearchCondition(dbQuery, collection, name, value) {
|
|
4
|
-
return dbQuery.
|
|
3
|
+
addSearchCondition(dbQuery, collection, name, value, logical) {
|
|
4
|
+
return dbQuery[logical].where({ [`${collection}.${name}`]: value });
|
|
5
5
|
}
|
|
6
6
|
isNumberValid(_value, _info) {
|
|
7
7
|
return true;
|
package/dist/database/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { performance } from 'perf_hooks';
|
|
|
11
11
|
import { promisify } from 'util';
|
|
12
12
|
import { getExtensionsPath } from '../extensions/lib/get-extensions-path.js';
|
|
13
13
|
import { useLogger } from '../logger/index.js';
|
|
14
|
+
import { useMetrics } from '../metrics/index.js';
|
|
14
15
|
import { getConfigFromEnv } from '../utils/get-config-from-env.js';
|
|
15
16
|
import { validateEnv } from '../utils/validate-env.js';
|
|
16
17
|
import { getHelpers } from './helpers/index.js';
|
|
@@ -25,6 +26,7 @@ export function getDatabase() {
|
|
|
25
26
|
}
|
|
26
27
|
const env = useEnv();
|
|
27
28
|
const logger = useLogger();
|
|
29
|
+
const metrics = useMetrics();
|
|
28
30
|
const { client, version, searchPath, connectionString, pool: poolConfig = {}, ...connectionConfig } = getConfigFromEnv('DB_', ['DB_EXCLUDE_TABLES']);
|
|
29
31
|
const requiredEnvVars = ['DB_CLIENT'];
|
|
30
32
|
switch (client) {
|
|
@@ -148,6 +150,7 @@ export function getDatabase() {
|
|
|
148
150
|
if (time) {
|
|
149
151
|
delta = performance.now() - time;
|
|
150
152
|
times.delete(queryInfo.__knexUid);
|
|
153
|
+
metrics?.getDatabaseResponseMetric()?.observe(delta);
|
|
151
154
|
}
|
|
152
155
|
// eslint-disable-next-line no-nested-ternary
|
|
153
156
|
const bindings = queryInfo.bindings
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/use-metrics.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/use-metrics.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MetricObjectWithValues, MetricValue } from 'prom-client';
|
|
2
|
+
import { Counter, Histogram } from 'prom-client';
|
|
3
|
+
export declare function createMetrics(): {
|
|
4
|
+
getDatabaseErrorMetric: () => Counter | null;
|
|
5
|
+
getDatabaseResponseMetric: () => Histogram | null;
|
|
6
|
+
getCacheErrorMetric: () => Counter | null;
|
|
7
|
+
getRedisErrorMetric: () => Counter | null;
|
|
8
|
+
getStorageErrorMetric: (location: string) => Counter | null;
|
|
9
|
+
aggregate: (data: {
|
|
10
|
+
pid: number;
|
|
11
|
+
metrics: MetricObjectWithValues<MetricValue<string>>[];
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
generate: () => Promise<void>;
|
|
14
|
+
readAll: () => Promise<string>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toArray } from '@directus/utils';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import pm2 from 'pm2';
|
|
7
|
+
import { AggregatorRegistry, Counter, Histogram, register } from 'prom-client';
|
|
8
|
+
import { getCache } from '../../cache.js';
|
|
9
|
+
import { hasDatabaseConnection } from '../../database/index.js';
|
|
10
|
+
import { redisConfigAvailable, useRedis } from '../../redis/index.js';
|
|
11
|
+
import { getStorage } from '../../storage/index.js';
|
|
12
|
+
const isPM2 = 'PM2_HOME' in process.env;
|
|
13
|
+
const METRICS_SYNC_PACKET = 'directus:metrics---data-sync';
|
|
14
|
+
const listApps = promisify(pm2.list.bind(pm2));
|
|
15
|
+
const sendDataToProcessId = promisify(pm2.sendDataToProcessId.bind(pm2));
|
|
16
|
+
export function createMetrics() {
|
|
17
|
+
const env = useEnv();
|
|
18
|
+
const services = env['METRICS_SERVICES'] ?? [];
|
|
19
|
+
const aggregates = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Listen for PM2 metric data sync messages and add them to the aggregate
|
|
22
|
+
*/
|
|
23
|
+
if (isPM2) {
|
|
24
|
+
process.on('message', (packet) => {
|
|
25
|
+
if (!packet.data || packet.topic !== METRICS_SYNC_PACKET)
|
|
26
|
+
return;
|
|
27
|
+
aggregate(packet.data);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function generate() {
|
|
31
|
+
const checkId = randomUUID();
|
|
32
|
+
await Promise.all([
|
|
33
|
+
trackDatabaseMetric(),
|
|
34
|
+
trackCacheMetric(checkId),
|
|
35
|
+
trackRedisMetric(checkId),
|
|
36
|
+
trackStorageMetric(checkId),
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* Push generated metrics to all pm2 instances
|
|
40
|
+
*/
|
|
41
|
+
if (isPM2) {
|
|
42
|
+
try {
|
|
43
|
+
const apps = await listApps();
|
|
44
|
+
const data = await register.getMetricsAsJSON();
|
|
45
|
+
const syncs = [];
|
|
46
|
+
for (const app of apps) {
|
|
47
|
+
if (app.pm_id === undefined || app.pid === 0 || app.name !== 'directus') {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
syncs.push(sendDataToProcessId(app.pm_id, {
|
|
51
|
+
data: { pid: process.pid, metrics: data },
|
|
52
|
+
topic: METRICS_SYNC_PACKET,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
await Promise.allSettled(syncs);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Add PM2 synced metric to the aggregate store.
|
|
64
|
+
* Subsequent syncs for the given instance will override previous value.
|
|
65
|
+
*/
|
|
66
|
+
async function aggregate(data) {
|
|
67
|
+
aggregates.set(data.pid, data.metrics);
|
|
68
|
+
}
|
|
69
|
+
async function readAll() {
|
|
70
|
+
/**
|
|
71
|
+
* In a PM2 context we must aggregate the metrics across instances ensuring
|
|
72
|
+
* only currently active instances are added to the aggregate
|
|
73
|
+
*/
|
|
74
|
+
if (isPM2 && aggregates.size !== 0) {
|
|
75
|
+
const apps = await listApps();
|
|
76
|
+
const aggregate = [];
|
|
77
|
+
for (const app of apps) {
|
|
78
|
+
if (aggregates.has(app.pid)) {
|
|
79
|
+
aggregate.push(aggregates.get(app.pid));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (aggregate.length !== 0) {
|
|
83
|
+
return AggregatorRegistry.aggregate(aggregate).metrics();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return register.metrics();
|
|
87
|
+
}
|
|
88
|
+
function getDatabaseErrorMetric() {
|
|
89
|
+
if (services.includes('database') === false) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const client = env['DB_CLIENT'];
|
|
93
|
+
let metric = register.getSingleMetric(`directus_db_${client}_connection_errors`);
|
|
94
|
+
if (!metric) {
|
|
95
|
+
metric = new Counter({
|
|
96
|
+
name: `directus_db_${client}_connection_errors`,
|
|
97
|
+
help: `${client} Database connection error count`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return metric;
|
|
101
|
+
}
|
|
102
|
+
function getDatabaseResponseMetric() {
|
|
103
|
+
if (services.includes('database') === false) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const client = env['DB_CLIENT'];
|
|
107
|
+
let metric = register.getSingleMetric(`directus_db_${client}_response_time_ms`);
|
|
108
|
+
if (!metric) {
|
|
109
|
+
metric = new Histogram({
|
|
110
|
+
name: `directus_db_${client}_response_time_ms`,
|
|
111
|
+
help: `${client} Database connection response time`,
|
|
112
|
+
buckets: [1, 10, 20, 40, 60, 80, 100, 200, 500, 750, 1000],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return metric;
|
|
116
|
+
}
|
|
117
|
+
function getCacheErrorMetric() {
|
|
118
|
+
if (services.includes('cache') === false || env['CACHE_ENABLED'] !== true) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (env['CACHE_STORE'] === 'redis' && redisConfigAvailable() !== true) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
let metric = register.getSingleMetric(`directus_cache_${env['CACHE_STORE']}_connection_errors`);
|
|
125
|
+
if (!metric) {
|
|
126
|
+
metric = new Counter({
|
|
127
|
+
name: `directus_cache_${env['CACHE_STORE']}_connection_errors`,
|
|
128
|
+
help: 'Cache connection error count',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return metric;
|
|
132
|
+
}
|
|
133
|
+
function getRedisErrorMetric() {
|
|
134
|
+
if (services.includes('redis') === false || redisConfigAvailable() !== true) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
let metric = register.getSingleMetric('directus_redis_connection_errors');
|
|
138
|
+
if (!metric) {
|
|
139
|
+
metric = new Counter({
|
|
140
|
+
name: `directus_redis_connection_errors`,
|
|
141
|
+
help: 'Redis connection error count',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return metric;
|
|
145
|
+
}
|
|
146
|
+
function getStorageErrorMetric(location) {
|
|
147
|
+
if (services.includes('storage') === false) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
let metric = register.getSingleMetric(`directus_storage_${location}_connection_errors`);
|
|
151
|
+
if (!metric) {
|
|
152
|
+
metric = new Counter({
|
|
153
|
+
name: `directus_storage_${location}_connection_errors`,
|
|
154
|
+
help: `${location} storage connection error count`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return metric;
|
|
158
|
+
}
|
|
159
|
+
async function trackDatabaseMetric() {
|
|
160
|
+
const metric = getDatabaseErrorMetric();
|
|
161
|
+
if (metric === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!(await hasDatabaseConnection())) {
|
|
166
|
+
metric.inc();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
metric.inc();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function trackCacheMetric(checkId) {
|
|
174
|
+
const metric = getCacheErrorMetric();
|
|
175
|
+
if (metric === null) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { cache } = getCache();
|
|
179
|
+
if (!cache) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await cache.set(`metrics-${checkId}`, '1', 5);
|
|
184
|
+
await cache.delete(`metrics-${checkId}`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
metric.inc();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function trackRedisMetric(checkId) {
|
|
191
|
+
const metric = getRedisErrorMetric();
|
|
192
|
+
if (metric === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const redis = useRedis();
|
|
196
|
+
try {
|
|
197
|
+
await redis.set(`metrics-${checkId}`, '1');
|
|
198
|
+
await redis.del(`metrics-${checkId}`);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
metric.inc();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function trackStorageMetric(checkId) {
|
|
205
|
+
if (services.includes('storage') === false) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const storage = await getStorage();
|
|
209
|
+
for (const location of toArray(env['STORAGE_LOCATIONS'])) {
|
|
210
|
+
const disk = storage.location(location);
|
|
211
|
+
const metric = getStorageErrorMetric(location);
|
|
212
|
+
if (metric === null) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await disk.write(`metric-${checkId}`, Readable.from(['check']));
|
|
217
|
+
const fileStream = await disk.read(`metric-${checkId}`);
|
|
218
|
+
await new Promise((resolve) => fileStream.on('data', async () => {
|
|
219
|
+
fileStream.destroy();
|
|
220
|
+
await disk.delete(`metric-${checkId}`);
|
|
221
|
+
return resolve(null);
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
metric.inc();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
getDatabaseErrorMetric,
|
|
231
|
+
getDatabaseResponseMetric,
|
|
232
|
+
getCacheErrorMetric,
|
|
233
|
+
getRedisErrorMetric,
|
|
234
|
+
getStorageErrorMetric,
|
|
235
|
+
aggregate,
|
|
236
|
+
generate,
|
|
237
|
+
readAll,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createMetrics } from './create-metrics.js';
|
|
2
|
+
export declare const _cache: {
|
|
3
|
+
metrics: ReturnType<typeof createMetrics> | undefined;
|
|
4
|
+
};
|
|
5
|
+
export declare const useMetrics: () => {
|
|
6
|
+
getDatabaseErrorMetric: () => import("prom-client").Counter | null;
|
|
7
|
+
getDatabaseResponseMetric: () => import("prom-client").Histogram | null;
|
|
8
|
+
getCacheErrorMetric: () => import("prom-client").Counter | null;
|
|
9
|
+
getRedisErrorMetric: () => import("prom-client").Counter | null;
|
|
10
|
+
getStorageErrorMetric: (location: string) => import("prom-client").Counter | null;
|
|
11
|
+
aggregate: (data: {
|
|
12
|
+
pid: number;
|
|
13
|
+
metrics: import("prom-client").MetricObjectWithValues<import("prom-client").MetricValue<string>>[];
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
generate: () => Promise<void>;
|
|
16
|
+
readAll: () => Promise<string>;
|
|
17
|
+
} | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toBoolean } from '@directus/utils';
|
|
3
|
+
import { createMetrics } from './create-metrics.js';
|
|
4
|
+
export const _cache = { metrics: undefined };
|
|
5
|
+
export const useMetrics = () => {
|
|
6
|
+
const env = useEnv();
|
|
7
|
+
if (!toBoolean(env['METRICS_ENABLED'])) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (_cache.metrics) {
|
|
11
|
+
return _cache.metrics;
|
|
12
|
+
}
|
|
13
|
+
_cache.metrics = createMetrics();
|
|
14
|
+
return _cache.metrics;
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type MetricService = 'database' | 'cache' | 'redis' | 'storage';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|