@directus/api 24.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/app.js +10 -4
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cache.d.ts +2 -2
  5. package/dist/cache.js +20 -7
  6. package/dist/controllers/assets.js +2 -2
  7. package/dist/controllers/metrics.d.ts +2 -0
  8. package/dist/controllers/metrics.js +33 -0
  9. package/dist/controllers/server.js +1 -1
  10. package/dist/database/get-ast-from-query/lib/parse-fields.js +4 -3
  11. package/dist/database/helpers/index.d.ts +1 -3
  12. package/dist/database/helpers/index.js +1 -3
  13. package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
  14. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  15. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  16. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  17. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  18. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  19. package/dist/database/helpers/number/types.d.ts +2 -2
  20. package/dist/database/helpers/number/types.js +2 -2
  21. package/dist/database/helpers/schema/dialects/oracle.d.ts +6 -1
  22. package/dist/database/helpers/schema/dialects/oracle.js +15 -0
  23. package/dist/database/helpers/schema/types.d.ts +3 -1
  24. package/dist/database/helpers/schema/types.js +9 -0
  25. package/dist/database/index.js +3 -0
  26. package/dist/metrics/index.d.ts +1 -0
  27. package/dist/metrics/index.js +1 -0
  28. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  29. package/dist/metrics/lib/create-metrics.js +239 -0
  30. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  31. package/dist/metrics/lib/use-metrics.js +15 -0
  32. package/dist/metrics/types/metric.d.ts +1 -0
  33. package/dist/metrics/types/metric.js +1 -0
  34. package/dist/middleware/respond.js +7 -1
  35. package/dist/operations/condition/index.js +7 -2
  36. package/dist/operations/mail/index.d.ts +6 -3
  37. package/dist/operations/mail/index.js +2 -2
  38. package/dist/schedules/metrics.d.ts +7 -0
  39. package/dist/schedules/metrics.js +44 -0
  40. package/dist/services/assets.d.ts +6 -1
  41. package/dist/services/assets.js +8 -6
  42. package/dist/services/fields.js +23 -39
  43. package/dist/services/files.js +6 -5
  44. package/dist/services/graphql/errors/format.d.ts +6 -0
  45. package/dist/services/graphql/errors/format.js +14 -0
  46. package/dist/services/graphql/index.d.ts +5 -53
  47. package/dist/services/graphql/index.js +5 -2720
  48. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  49. package/dist/services/graphql/resolvers/mutation.js +74 -0
  50. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  51. package/dist/services/graphql/resolvers/query.js +87 -0
  52. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  53. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  54. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  55. package/dist/services/graphql/resolvers/system-global.js +435 -0
  56. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  57. package/dist/services/graphql/resolvers/system.js +554 -0
  58. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  59. package/dist/services/graphql/schema/get-types.js +223 -0
  60. package/dist/services/graphql/schema/index.d.ts +32 -0
  61. package/dist/services/graphql/schema/index.js +190 -0
  62. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  63. package/dist/services/graphql/schema/parse-args.js +35 -0
  64. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  65. package/dist/services/graphql/schema/parse-query.js +98 -0
  66. package/dist/services/graphql/schema/read.d.ts +12 -0
  67. package/dist/services/graphql/schema/read.js +653 -0
  68. package/dist/services/graphql/schema/write.d.ts +9 -0
  69. package/dist/services/graphql/schema/write.js +142 -0
  70. package/dist/services/graphql/subscription.d.ts +1 -1
  71. package/dist/services/graphql/subscription.js +7 -6
  72. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  73. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  74. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  75. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  76. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  77. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  78. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  79. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  80. package/dist/services/items.js +0 -2
  81. package/dist/services/meta.js +25 -84
  82. package/dist/services/users.d.ts +4 -0
  83. package/dist/services/users.js +23 -1
  84. package/dist/utils/apply-query.d.ts +1 -1
  85. package/dist/utils/apply-query.js +58 -21
  86. package/dist/utils/freeze-schema.d.ts +3 -0
  87. package/dist/utils/freeze-schema.js +31 -0
  88. package/dist/utils/get-accountability-for-token.js +1 -0
  89. package/dist/utils/get-milliseconds.js +1 -1
  90. package/dist/utils/get-schema.js +11 -6
  91. package/dist/utils/permissions-cachable.d.ts +8 -0
  92. package/dist/utils/permissions-cachable.js +38 -0
  93. package/dist/utils/sanitize-schema.d.ts +1 -1
  94. package/dist/websocket/messages.d.ts +6 -6
  95. package/package.json +23 -20
  96. package/dist/database/helpers/nullable-update/dialects/default.d.ts +0 -3
  97. package/dist/database/helpers/nullable-update/dialects/default.js +0 -3
  98. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +0 -12
  99. package/dist/database/helpers/nullable-update/dialects/oracle.js +0 -16
  100. package/dist/database/helpers/nullable-update/index.d.ts +0 -7
  101. package/dist/database/helpers/nullable-update/index.js +0 -7
  102. package/dist/database/helpers/nullable-update/types.d.ts +0 -7
  103. package/dist/database/helpers/nullable-update/types.js +0 -12
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 = jwt.verify(req.cookies[`oauth2.${providerName}`], getSecret(), {
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 = jwt.verify(req.cookies[`openid.${providerName}`], getSecret(), {
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 setLocalSchemaCache(schema: SchemaOverview): Promise<void>;
17
- export declare function getLocalSchemaCache(): Promise<SchemaOverview | undefined>;
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 async function setLocalSchemaCache(schema) {
79
- const { localSchemaCache } = getCache();
80
- await localSchemaCache.set('schema', schema);
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 async function getLocalSchemaCache() {
83
- const { localSchemaCache } = getCache();
84
- return await localSchemaCache.get('schema');
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,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -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';
@@ -169,10 +169,11 @@ export async function parseFields(options, context) {
169
169
  continue;
170
170
  }
171
171
  }
172
+ const childQuery = { ...options.query };
172
173
  // update query alias for children parseFields
173
174
  const deepAlias = getDeepQuery(options.deep?.[fieldKey] || {})?.['alias'];
174
- if (!isEmpty(deepAlias))
175
- options.query.alias = deepAlias;
175
+ // reset alias to empty if none are present
176
+ childQuery.alias = isEmpty(deepAlias) ? {} : deepAlias;
176
177
  child = {
177
178
  type: relationType,
178
179
  name: relatedCollection,
@@ -184,7 +185,7 @@ export async function parseFields(options, context) {
184
185
  children: await parseFields({
185
186
  parentCollection: relatedCollection,
186
187
  fields: nestedFields,
187
- query: options.query,
188
+ query: childQuery,
188
189
  deep: options.deep?.[fieldKey] || {},
189
190
  accountability: options.accountability,
190
191
  }, context),
@@ -3,17 +3,15 @@ import type { Knex } from 'knex';
3
3
  import * as dateHelpers from './date/index.js';
4
4
  import * as fnHelpers from './fn/index.js';
5
5
  import * as geometryHelpers from './geometry/index.js';
6
+ import * as numberHelpers from './number/index.js';
6
7
  import * as schemaHelpers from './schema/index.js';
7
8
  import * as sequenceHelpers from './sequence/index.js';
8
- import * as numberHelpers from './number/index.js';
9
- import * as nullableUpdateHelper from './nullable-update/index.js';
10
9
  export declare function getHelpers(database: Knex): {
11
10
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
12
11
  st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
13
12
  schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
14
13
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
15
14
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
16
- nullableUpdate: nullableUpdateHelper.postgres | nullableUpdateHelper.oracle;
17
15
  };
18
16
  export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
19
17
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -2,10 +2,9 @@ import { getDatabaseClient } from '../index.js';
2
2
  import * as dateHelpers from './date/index.js';
3
3
  import * as fnHelpers from './fn/index.js';
4
4
  import * as geometryHelpers from './geometry/index.js';
5
+ import * as numberHelpers from './number/index.js';
5
6
  import * as schemaHelpers from './schema/index.js';
6
7
  import * as sequenceHelpers from './sequence/index.js';
7
- import * as numberHelpers from './number/index.js';
8
- import * as nullableUpdateHelper from './nullable-update/index.js';
9
8
  export function getHelpers(database) {
10
9
  const client = getDatabaseClient(database);
11
10
  return {
@@ -14,7 +13,6 @@ export function getHelpers(database) {
14
13
  schema: new schemaHelpers[client](database),
15
14
  sequence: new sequenceHelpers[client](database),
16
15
  number: new numberHelpers[client](database),
17
- nullableUpdate: new nullableUpdateHelper[client](database),
18
16
  };
19
17
  }
20
18
  export function getFunctions(database, schema) {
@@ -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.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
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.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
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.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
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.orWhere({ [`${collection}.${name}`]: value });
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;
@@ -1,5 +1,6 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
- import type { Field, Relation, Type } from '@directus/types';
2
+ import type { Column } from '@directus/schema';
3
+ import type { Field, RawField, Relation, Type } from '@directus/types';
3
4
  import type { Knex } from 'knex';
4
5
  import type { Options, SortRecord, Sql } from '../types.js';
5
6
  import { SchemaHelper } from '../types.js';
@@ -10,6 +11,10 @@ export declare class SchemaHelperOracle extends SchemaHelper {
10
11
  preRelationChange(relation: Partial<Relation>): void;
11
12
  processFieldType(field: Field): Type;
12
13
  getDatabaseSize(): Promise<number | null>;
14
+ /**
15
+ * Oracle throws an error when overwriting the nullable option for an existing column with the same value.
16
+ */
17
+ setNullable(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column | null): void;
13
18
  prepQueryParams(queryParams: Sql): Sql;
14
19
  prepBindings(bindings: Knex.Value[]): any;
15
20
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
@@ -52,6 +52,21 @@ export class SchemaHelperOracle extends SchemaHelper {
52
52
  return null;
53
53
  }
54
54
  }
55
+ /**
56
+ * Oracle throws an error when overwriting the nullable option for an existing column with the same value.
57
+ */
58
+ setNullable(column, field, existing) {
59
+ if (!existing) {
60
+ super.setNullable(column, field, existing);
61
+ return;
62
+ }
63
+ if (field.schema?.is_nullable === false && existing.is_nullable === true) {
64
+ column.notNullable();
65
+ }
66
+ else if (field.schema?.is_nullable === true && existing.is_nullable === false) {
67
+ column.nullable();
68
+ }
69
+ }
55
70
  prepQueryParams(queryParams) {
56
71
  return prepQueryParams(queryParams, { format: (index) => `:${index + 1}` });
57
72
  }
@@ -1,5 +1,6 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
- import type { Field, Relation, Type } from '@directus/types';
2
+ import type { Column } from '@directus/schema';
3
+ import type { Field, RawField, Relation, Type } from '@directus/types';
3
4
  import type { Knex } from 'knex';
4
5
  import type { DatabaseClient } from '../../../types/index.js';
5
6
  import { DatabaseHelper } from '../types.js';
@@ -25,6 +26,7 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
25
26
  preColumnChange(): Promise<boolean>;
26
27
  postColumnChange(): Promise<void>;
27
28
  preRelationChange(_relation: Partial<Relation>): void;
29
+ setNullable(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column | null): void;
28
30
  processFieldType(field: Field): Type;
29
31
  constraintName(existingName: string): string;
30
32
  applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
@@ -66,6 +66,15 @@ export class SchemaHelper extends DatabaseHelper {
66
66
  preRelationChange(_relation) {
67
67
  return;
68
68
  }
69
+ setNullable(column, field, existing) {
70
+ const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
71
+ if (isNullable) {
72
+ column.nullable();
73
+ }
74
+ else {
75
+ column.notNullable();
76
+ }
77
+ }
69
78
  processFieldType(field) {
70
79
  return field.type;
71
80
  }
@@ -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
+ };