@directus/api 24.0.1 → 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 (85) 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/helpers/number/dialects/mssql.d.ts +2 -2
  11. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  12. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  13. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  14. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  15. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  16. package/dist/database/helpers/number/types.d.ts +2 -2
  17. package/dist/database/helpers/number/types.js +2 -2
  18. package/dist/database/index.js +3 -0
  19. package/dist/metrics/index.d.ts +1 -0
  20. package/dist/metrics/index.js +1 -0
  21. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  22. package/dist/metrics/lib/create-metrics.js +239 -0
  23. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  24. package/dist/metrics/lib/use-metrics.js +15 -0
  25. package/dist/metrics/types/metric.d.ts +1 -0
  26. package/dist/metrics/types/metric.js +1 -0
  27. package/dist/middleware/respond.js +7 -1
  28. package/dist/operations/condition/index.js +7 -2
  29. package/dist/schedules/metrics.d.ts +7 -0
  30. package/dist/schedules/metrics.js +44 -0
  31. package/dist/services/assets.d.ts +6 -1
  32. package/dist/services/assets.js +8 -6
  33. package/dist/services/fields.js +1 -1
  34. package/dist/services/graphql/errors/format.d.ts +6 -0
  35. package/dist/services/graphql/errors/format.js +14 -0
  36. package/dist/services/graphql/index.d.ts +5 -53
  37. package/dist/services/graphql/index.js +5 -2720
  38. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  39. package/dist/services/graphql/resolvers/mutation.js +74 -0
  40. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  41. package/dist/services/graphql/resolvers/query.js +87 -0
  42. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  43. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  44. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  45. package/dist/services/graphql/resolvers/system-global.js +435 -0
  46. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  47. package/dist/services/graphql/resolvers/system.js +554 -0
  48. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  49. package/dist/services/graphql/schema/get-types.js +223 -0
  50. package/dist/services/graphql/schema/index.d.ts +32 -0
  51. package/dist/services/graphql/schema/index.js +190 -0
  52. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  53. package/dist/services/graphql/schema/parse-args.js +35 -0
  54. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  55. package/dist/services/graphql/schema/parse-query.js +98 -0
  56. package/dist/services/graphql/schema/read.d.ts +12 -0
  57. package/dist/services/graphql/schema/read.js +653 -0
  58. package/dist/services/graphql/schema/write.d.ts +9 -0
  59. package/dist/services/graphql/schema/write.js +142 -0
  60. package/dist/services/graphql/subscription.d.ts +1 -1
  61. package/dist/services/graphql/subscription.js +7 -6
  62. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  63. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  64. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  65. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  66. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  67. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  68. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  69. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  70. package/dist/services/items.js +0 -2
  71. package/dist/services/meta.js +25 -84
  72. package/dist/services/users.d.ts +4 -0
  73. package/dist/services/users.js +23 -1
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/apply-query.js +58 -21
  76. package/dist/utils/freeze-schema.d.ts +3 -0
  77. package/dist/utils/freeze-schema.js +31 -0
  78. package/dist/utils/get-accountability-for-token.js +1 -0
  79. package/dist/utils/get-milliseconds.js +1 -1
  80. package/dist/utils/get-schema.js +10 -5
  81. package/dist/utils/permissions-cachable.d.ts +8 -0
  82. package/dist/utils/permissions-cachable.js +38 -0
  83. package/dist/utils/sanitize-schema.d.ts +1 -1
  84. package/dist/websocket/messages.d.ts +6 -6
  85. package/package.json +22 -19
@@ -33,9 +33,6 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
33
33
  hasJoins = sortResult.hasJoins;
34
34
  }
35
35
  }
36
- if (query.search) {
37
- applySearch(knex, schema, dbQuery, query.search, collection);
38
- }
39
36
  // `cases` are the permissions cases that are required for the current data set. We're
40
37
  // dynamically adding those into the filters that the user provided to enforce the permission
41
38
  // rules. You should be able to read an item if one or more of the cases matches. The actual case
@@ -81,6 +78,9 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
81
78
  }
82
79
  dbQuery.groupBy(columns);
83
80
  }
81
+ if (query.search) {
82
+ applySearch(knex, schema, dbQuery, query.search, collection, aliasMap, permissions);
83
+ }
84
84
  if (query.aggregate) {
85
85
  applyAggregate(schema, dbQuery, query.aggregate, collection, hasJoins);
86
86
  }
@@ -608,35 +608,72 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
608
608
  }
609
609
  }
610
610
  }
611
- export function applySearch(knex, schema, dbQuery, searchQuery, collection) {
611
+ export function applySearch(knex, schema, dbQuery, searchQuery, collection, aliasMap, permissions) {
612
612
  const { number: numberHelper } = getHelpers(knex);
613
- const fields = Object.entries(schema.collections[collection].fields);
614
- dbQuery.andWhere(function () {
613
+ const allowedFields = new Set(permissions.filter((p) => p.collection === collection).flatMap((p) => p.fields ?? []));
614
+ let fields = Object.entries(schema.collections[collection].fields);
615
+ const { cases, caseMap } = getCases(collection, permissions, []);
616
+ // Add field restrictions if non-admin and "everything" is not allowed
617
+ if (cases.length !== 0 && !allowedFields.has('*')) {
618
+ fields = fields.filter((field) => allowedFields.has(field[0]));
619
+ }
620
+ dbQuery.andWhere(function (queryBuilder) {
615
621
  let needsFallbackCondition = true;
616
622
  fields.forEach(([name, field]) => {
617
- if (['text', 'string'].includes(field.type)) {
618
- this.orWhereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
623
+ const whenCases = (caseMap[name] ?? []).map((caseIndex) => cases[caseIndex]);
624
+ const fieldType = getFieldType(field);
625
+ if (fieldType !== null) {
619
626
  needsFallbackCondition = false;
620
627
  }
621
- else if (isNumericField(field)) {
622
- const number = parseNumericString(searchQuery);
623
- if (number === null) {
624
- return; // unable to parse
625
- }
626
- if (numberHelper.isNumberValid(number, field)) {
627
- numberHelper.addSearchCondition(this, collection, name, number);
628
- needsFallbackCondition = false;
629
- }
628
+ else {
629
+ return;
630
630
  }
631
- else if (field.type === 'uuid' && isValidUuid(searchQuery)) {
632
- this.orWhere({ [`${collection}.${name}`]: searchQuery });
633
- needsFallbackCondition = false;
631
+ if (cases.length !== 0 && whenCases?.length !== 0) {
632
+ queryBuilder.orWhere((subQuery) => {
633
+ addSearchCondition(subQuery, name, fieldType, 'and');
634
+ applyFilter(knex, schema, subQuery, { _or: whenCases }, collection, aliasMap, cases, permissions);
635
+ });
636
+ }
637
+ else {
638
+ addSearchCondition(queryBuilder, name, fieldType, 'or');
634
639
  }
635
640
  });
636
641
  if (needsFallbackCondition) {
637
- this.orWhereRaw('1 = 0');
642
+ queryBuilder.orWhereRaw('1 = 0');
638
643
  }
639
644
  });
645
+ function addSearchCondition(queryBuilder, name, fieldType, logical) {
646
+ if (fieldType === null) {
647
+ return;
648
+ }
649
+ if (fieldType === 'string') {
650
+ queryBuilder[logical].whereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
651
+ }
652
+ else if (fieldType === 'numeric') {
653
+ numberHelper.addSearchCondition(queryBuilder, collection, name, parseNumericString(searchQuery), logical);
654
+ }
655
+ else if (fieldType === 'uuid') {
656
+ queryBuilder[logical].where({ [`${collection}.${name}`]: searchQuery });
657
+ }
658
+ }
659
+ function getFieldType(field) {
660
+ if (['text', 'string'].includes(field.type)) {
661
+ return 'string';
662
+ }
663
+ if (isNumericField(field)) {
664
+ const number = parseNumericString(searchQuery);
665
+ if (number === null) {
666
+ return null;
667
+ }
668
+ if (numberHelper.isNumberValid(number, field)) {
669
+ return 'numeric';
670
+ }
671
+ }
672
+ if (field.type === 'uuid' && isValidUuid(searchQuery)) {
673
+ return 'uuid';
674
+ }
675
+ return null;
676
+ }
640
677
  }
641
678
  export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins) {
642
679
  for (const [operation, fields] of Object.entries(aggregate)) {
@@ -0,0 +1,3 @@
1
+ import type { SchemaOverview } from '@directus/types';
2
+ export declare function freezeSchema(schema: SchemaOverview): Readonly<SchemaOverview>;
3
+ export declare function unfreezeSchema(schema: Readonly<SchemaOverview>): SchemaOverview;
@@ -0,0 +1,31 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ export function freezeSchema(schema) {
3
+ // freeze collections
4
+ for (const collectionName of Object.keys(schema.collections)) {
5
+ if (!schema.collections[collectionName])
6
+ continue;
7
+ for (const fieldName of Object.keys(schema.collections[collectionName].fields)) {
8
+ Object.freeze(schema.collections[collectionName].fields[fieldName]);
9
+ }
10
+ Object.freeze(schema.collections[collectionName]);
11
+ }
12
+ Object.freeze(schema.collections);
13
+ // freeze relations
14
+ for (const relation of schema.relations) {
15
+ if (relation.schema)
16
+ Object.freeze(relation.schema);
17
+ if (relation.meta)
18
+ Object.freeze(relation.meta);
19
+ Object.freeze(relation);
20
+ }
21
+ Object.freeze(schema.relations);
22
+ return Object.freeze(schema);
23
+ }
24
+ export function unfreezeSchema(schema) {
25
+ if (Object.isFrozen(schema)) {
26
+ return cloneDeep(schema);
27
+ }
28
+ else {
29
+ return schema;
30
+ }
31
+ }
@@ -18,6 +18,7 @@ export async function getAccountabilityForToken(token, accountability) {
18
18
  const payload = verifyAccessJWT(token, getSecret());
19
19
  if ('session' in payload) {
20
20
  await verifySessionJWT(payload);
21
+ accountability.session = payload.session;
21
22
  }
22
23
  if (payload.share)
23
24
  accountability.share = payload.share;
@@ -1,4 +1,4 @@
1
- import ms from 'ms';
1
+ import ms, {} from 'ms';
2
2
  /**
3
3
  * Safely parse human readable time format into milliseconds
4
4
  */
@@ -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 { getLocalSchemaCache, setLocalSchemaCache } from '../cache.js';
7
+ import { getMemorySchemaCache, setMemorySchemaCache } 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 getLocalSchemaCache();
25
+ const cached = getMemorySchemaCache();
26
26
  if (cached) {
27
27
  return cached;
28
28
  }
@@ -48,8 +48,13 @@ export async function getSchema(options, attempt = 0) {
48
48
  if (options.schema === null) {
49
49
  return reject();
50
50
  }
51
- setLocalSchemaCache(options.schema).catch(reject);
52
- resolve(options.schema);
51
+ try {
52
+ setMemorySchemaCache(options.schema);
53
+ resolve(options.schema);
54
+ }
55
+ catch (e) {
56
+ reject(e);
57
+ }
53
58
  }
54
59
  function cleanup() {
55
60
  bus.unsubscribe(messageKey, busListener).catch(reject);
@@ -62,7 +67,7 @@ export async function getSchema(options, attempt = 0) {
62
67
  const database = options?.database || getDatabase();
63
68
  const schemaInspector = createInspector(database);
64
69
  schema = await getDatabaseSchema(database, schemaInspector);
65
- await setLocalSchemaCache(schema);
70
+ setMemorySchemaCache(schema);
66
71
  return schema;
67
72
  }
68
73
  finally {
@@ -0,0 +1,8 @@
1
+ import type { Accountability, Filter } from '@directus/types';
2
+ import type { Context } from '../permissions/types.js';
3
+ /**
4
+ * Check if the read permissions for a collection contain the dynamic variable $NOW.
5
+ * If they do, the permissions are not cachable.
6
+ */
7
+ export declare function permissionsCachable(collection: string | undefined, context: Context, accountability?: Accountability): Promise<boolean>;
8
+ export declare function filter_has_now(filter: Filter): boolean;
@@ -0,0 +1,38 @@
1
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
2
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
3
+ import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
4
+ /**
5
+ * Check if the read permissions for a collection contain the dynamic variable $NOW.
6
+ * If they do, the permissions are not cachable.
7
+ */
8
+ export async function permissionsCachable(collection, context, accountability) {
9
+ if (!collection) {
10
+ return true;
11
+ }
12
+ if (!accountability) {
13
+ accountability = createDefaultAccountability();
14
+ }
15
+ const policies = await fetchPolicies(accountability, context);
16
+ const permissions = await fetchPermissions({ action: 'read', policies, collections: [collection], accountability, bypassDynamicVariableProcessing: true }, context);
17
+ const has_now = permissions.some((permission) => {
18
+ if (!permission.permissions) {
19
+ return false;
20
+ }
21
+ return filter_has_now(permission.permissions);
22
+ });
23
+ return !has_now;
24
+ }
25
+ export function filter_has_now(filter) {
26
+ return Object.entries(filter).some(([key, value]) => {
27
+ if (key === '_and' || key === '_or') {
28
+ return value.some((sub_filter) => filter_has_now(sub_filter));
29
+ }
30
+ else if (typeof value === 'object') {
31
+ return filter_has_now(value);
32
+ }
33
+ else if (typeof value === 'string') {
34
+ return value.startsWith('$NOW');
35
+ }
36
+ return false;
37
+ });
38
+ }
@@ -16,7 +16,7 @@ export declare function sanitizeCollection(collection: Collection | undefined):
16
16
  * @returns sanitized field
17
17
  */
18
18
  export declare function sanitizeField(field: Field | undefined, sanitizeAllSchema?: boolean): Partial<Field> | undefined;
19
- export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "data_type" | "default_value" | "max_length" | "numeric_precision" | "numeric_scale" | "is_nullable" | "is_unique" | "is_indexed" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
19
+ export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "foreign_key_table" | "foreign_key_column" | "name" | "data_type" | "default_value" | "max_length" | "numeric_precision" | "numeric_scale" | "is_nullable" | "is_unique" | "is_indexed" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment">;
20
20
  /**
21
21
  * Pick certain database vendor specific relation properties that should be compared when performing diff
22
22
  *
@@ -247,16 +247,16 @@ export declare const WebSocketItemsMessage: z.ZodUnion<[z.ZodObject<z.objectUtil
247
247
  collection: string;
248
248
  type: "items";
249
249
  action: "read";
250
- query?: Query | undefined;
251
250
  id?: string | number | undefined;
251
+ query?: Query | undefined;
252
252
  uid?: string | number | undefined;
253
253
  ids?: (string | number)[] | undefined;
254
254
  }, {
255
255
  collection: string;
256
256
  type: "items";
257
257
  action: "read";
258
- query?: Query | undefined;
259
258
  id?: string | number | undefined;
259
+ query?: Query | undefined;
260
260
  uid?: string | number | undefined;
261
261
  ids?: (string | number)[] | undefined;
262
262
  }>, z.ZodObject<z.objectUtil.extendShape<{
@@ -274,8 +274,8 @@ export declare const WebSocketItemsMessage: z.ZodUnion<[z.ZodObject<z.objectUtil
274
274
  type: "items";
275
275
  action: "update";
276
276
  data: Partial<Item>;
277
- query?: Query | undefined;
278
277
  id?: string | number | undefined;
278
+ query?: Query | undefined;
279
279
  uid?: string | number | undefined;
280
280
  ids?: (string | number)[] | undefined;
281
281
  }, {
@@ -283,8 +283,8 @@ export declare const WebSocketItemsMessage: z.ZodUnion<[z.ZodObject<z.objectUtil
283
283
  type: "items";
284
284
  action: "update";
285
285
  data: Partial<Item>;
286
- query?: Query | undefined;
287
286
  id?: string | number | undefined;
287
+ query?: Query | undefined;
288
288
  uid?: string | number | undefined;
289
289
  ids?: (string | number)[] | undefined;
290
290
  }>, z.ZodObject<z.objectUtil.extendShape<{
@@ -300,16 +300,16 @@ export declare const WebSocketItemsMessage: z.ZodUnion<[z.ZodObject<z.objectUtil
300
300
  collection: string;
301
301
  type: "items";
302
302
  action: "delete";
303
- query?: Query | undefined;
304
303
  id?: string | number | undefined;
304
+ query?: Query | undefined;
305
305
  uid?: string | number | undefined;
306
306
  ids?: (string | number)[] | undefined;
307
307
  }, {
308
308
  collection: string;
309
309
  type: "items";
310
310
  action: "delete";
311
- query?: Query | undefined;
312
311
  id?: string | number | undefined;
312
+ query?: Query | undefined;
313
313
  uid?: string | number | undefined;
314
314
  ids?: (string | number)[] | undefined;
315
315
  }>]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "24.0.1",
3
+ "version": "25.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -68,6 +68,7 @@
68
68
  "@tus/utils": "0.5.0",
69
69
  "argon2": "0.41.1",
70
70
  "async": "3.2.6",
71
+ "async-mutex": "0.5.0",
71
72
  "axios": "1.7.9",
72
73
  "busboy": "1.6.0",
73
74
  "bytes": "3.1.2",
@@ -133,6 +134,8 @@
133
134
  "pino-http": "10.3.0",
134
135
  "pino-http-print": "3.1.0",
135
136
  "pino-pretty": "13.0.0",
137
+ "pm2": "5.4.3",
138
+ "prom-client": "15.1.3",
136
139
  "qs": "6.13.1",
137
140
  "rate-limiter-flexible": "5.0.4",
138
141
  "rollup": "4.30.1",
@@ -147,29 +150,29 @@
147
150
  "ws": "8.18.0",
148
151
  "zod": "3.24.1",
149
152
  "zod-validation-error": "3.4.0",
150
- "@directus/app": "13.6.0",
151
153
  "@directus/constants": "13.0.0",
152
- "@directus/env": "5.0.0",
154
+ "@directus/app": "13.7.0",
155
+ "@directus/env": "5.0.1",
153
156
  "@directus/errors": "2.0.0",
154
- "@directus/extensions": "3.0.1",
155
- "@directus/extensions-registry": "3.0.1",
156
- "@directus/extensions-sdk": "13.0.1",
157
+ "@directus/extensions": "3.0.2",
158
+ "@directus/extensions-sdk": "13.0.2",
157
159
  "@directus/format-title": "12.0.0",
158
- "@directus/memory": "3.0.0",
160
+ "@directus/memory": "3.0.1",
161
+ "@directus/pressure": "3.0.1",
162
+ "@directus/extensions-registry": "3.0.2",
163
+ "@directus/storage": "12.0.0",
159
164
  "@directus/schema": "13.0.0",
160
165
  "@directus/specs": "11.1.0",
161
- "@directus/storage-driver-azure": "12.0.0",
162
- "@directus/pressure": "3.0.0",
163
- "@directus/storage": "12.0.0",
164
- "@directus/storage-driver-cloudinary": "12.0.0",
166
+ "@directus/storage-driver-azure": "12.0.1",
167
+ "@directus/storage-driver-gcs": "12.0.1",
168
+ "@directus/storage-driver-cloudinary": "12.0.1",
165
169
  "@directus/storage-driver-local": "12.0.0",
166
- "@directus/storage-driver-gcs": "12.0.0",
167
- "@directus/storage-driver-s3": "12.0.0",
168
- "@directus/storage-driver-supabase": "3.0.0",
169
- "@directus/utils": "13.0.0",
170
+ "@directus/storage-driver-supabase": "3.0.1",
171
+ "@directus/storage-driver-s3": "12.0.1",
170
172
  "@directus/system-data": "3.0.0",
171
- "@directus/validation": "2.0.0",
172
- "directus": "11.4.1"
173
+ "@directus/utils": "13.0.1",
174
+ "@directus/validation": "2.0.1",
175
+ "directus": "11.5.0"
173
176
  },
174
177
  "devDependencies": {
175
178
  "@directus/tsconfig": "3.0.0",
@@ -194,7 +197,7 @@
194
197
  "@types/ldapjs": "2.2.5",
195
198
  "@types/lodash-es": "4.17.12",
196
199
  "@types/mime-types": "2.1.4",
197
- "@types/ms": "0.7.34",
200
+ "@types/ms": "2.1.0",
198
201
  "@types/node": "22.10.5",
199
202
  "@types/node-schedule": "2.1.7",
200
203
  "@types/nodemailer": "6.4.17",
@@ -211,7 +214,7 @@
211
214
  "get-port": "7.1.0",
212
215
  "knex-mock-client": "3.0.2",
213
216
  "typescript": "5.7.3",
214
- "vitest": "2.1.8",
217
+ "vitest": "2.1.9",
215
218
  "@directus/random": "2.0.0",
216
219
  "@directus/types": "13.0.0"
217
220
  },