@directus/api 23.1.0 → 23.1.2

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 (37) hide show
  1. package/dist/controllers/permissions.js +1 -1
  2. package/dist/controllers/users.js +4 -8
  3. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  4. package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -2
  5. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  6. package/dist/database/helpers/schema/dialects/mssql.js +1 -1
  7. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -1
  8. package/dist/database/helpers/schema/dialects/mysql.js +2 -2
  9. package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/oracle.js +1 -1
  11. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  13. package/dist/database/helpers/schema/types.d.ts +1 -1
  14. package/dist/database/helpers/schema/types.js +1 -1
  15. package/dist/database/migrations/20240806A-permissions-policies.d.ts +0 -3
  16. package/dist/database/migrations/20240806A-permissions-policies.js +8 -94
  17. package/dist/database/run-ast/lib/get-db-query.js +1 -1
  18. package/dist/database/run-ast/utils/apply-case-when.js +1 -1
  19. package/dist/permissions/lib/fetch-permissions.d.ts +1 -1
  20. package/dist/permissions/lib/fetch-permissions.js +4 -1
  21. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  22. package/dist/permissions/modules/validate-access/validate-access.js +1 -1
  23. package/dist/permissions/utils/fetch-share-info.d.ts +12 -0
  24. package/dist/permissions/utils/fetch-share-info.js +9 -0
  25. package/dist/permissions/utils/get-permissions-for-share.d.ts +4 -0
  26. package/dist/permissions/utils/get-permissions-for-share.js +182 -0
  27. package/dist/permissions/utils/merge-permissions.d.ts +9 -0
  28. package/dist/permissions/utils/merge-permissions.js +118 -0
  29. package/dist/services/assets.js +1 -1
  30. package/dist/services/authentication.js +1 -10
  31. package/dist/services/collections.js +5 -6
  32. package/dist/services/comments.js +8 -4
  33. package/dist/services/shares.d.ts +2 -0
  34. package/dist/services/shares.js +11 -9
  35. package/dist/types/auth.d.ts +0 -7
  36. package/dist/utils/get-accountability-for-token.js +0 -2
  37. package/package.json +20 -20
@@ -72,7 +72,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
72
72
  router.get('/', validateBatch('read'), readHandler, respond);
73
73
  router.search('/', validateBatch('read'), readHandler, respond);
74
74
  router.get('/me', asyncHandler(async (req, res, next) => {
75
- if (!req.accountability?.user && !req.accountability?.role)
75
+ if (!req.accountability?.user && !req.accountability?.role && !req.accountability?.share)
76
76
  throw new ForbiddenError();
77
77
  const result = await fetchAccountabilityCollectionAccess(req.accountability, {
78
78
  schema: req.schema,
@@ -62,16 +62,12 @@ const readHandler = asyncHandler(async (req, res, next) => {
62
62
  router.get('/', validateBatch('read'), readHandler, respond);
63
63
  router.search('/', validateBatch('read'), readHandler, respond);
64
64
  router.get('/me', asyncHandler(async (req, res, next) => {
65
- if (req.accountability?.share_scope) {
66
- const user = {
67
- share: req.accountability?.share,
68
- role: {
69
- id: req.accountability.role,
70
- admin_access: false,
71
- app_access: false,
65
+ if (req.accountability?.share) {
66
+ res.locals['payload'] = {
67
+ data: {
68
+ share: req.accountability?.share,
72
69
  },
73
70
  };
74
- res.locals['payload'] = { data: user };
75
71
  return next();
76
72
  }
77
73
  if (!req.accountability?.user) {
@@ -7,5 +7,5 @@ export declare class SchemaHelperCockroachDb extends SchemaHelper {
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
9
  preprocessBindings(queryParams: Sql): Sql;
10
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
10
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
11
11
  }
@@ -32,8 +32,8 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
32
32
  preprocessBindings(queryParams) {
33
33
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
34
34
  }
35
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
36
- if (hasMultiRelationalSort) {
35
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
36
+ if (hasRelationalSort) {
37
37
  /*
38
38
  Cockroach allows aliases to be used in the GROUP BY clause and only needs columns in the GROUP BY clause that
39
39
  are not functionally dependent on the primary key.
@@ -6,5 +6,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
6
6
  formatUUID(uuid: string): string;
7
7
  getDatabaseSize(): Promise<number | null>;
8
8
  preprocessBindings(queryParams: Sql): Sql;
9
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
9
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
10
10
  }
@@ -30,7 +30,7 @@ export class SchemaHelperMSSQL extends SchemaHelper {
30
30
  preprocessBindings(queryParams) {
31
31
  return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
32
32
  }
33
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
33
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
34
34
  /*
35
35
  MSSQL requires all selected columns that are not aggregated over are to be present in the GROUP BY clause
36
36
 
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperMySQL extends SchemaHelper {
4
4
  applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
5
5
  getDatabaseSize(): Promise<number | null>;
6
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
7
  }
@@ -28,8 +28,8 @@ export class SchemaHelperMySQL extends SchemaHelper {
28
28
  return null;
29
29
  }
30
30
  }
31
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
32
- if (hasMultiRelationalSort) {
31
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
32
+ if (hasRelationalSort) {
33
33
  /*
34
34
  ** MySQL **
35
35
 
@@ -10,5 +10,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
10
10
  processFieldType(field: Field): Type;
11
11
  getDatabaseSize(): Promise<number | null>;
12
12
  preprocessBindings(queryParams: Sql): Sql;
13
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
13
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
14
14
  }
@@ -42,7 +42,7 @@ export class SchemaHelperOracle extends SchemaHelper {
42
42
  preprocessBindings(queryParams) {
43
43
  return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
44
44
  }
45
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
45
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
46
46
  /*
47
47
  Oracle requires all selected columns that are not aggregated over to be present in the GROUP BY clause
48
48
  aliases can not be used before version 23c.
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  getDatabaseSize(): Promise<number | null>;
5
5
  preprocessBindings(queryParams: Sql): Sql;
6
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
7
  }
@@ -15,12 +15,12 @@ export class SchemaHelperPostgres extends SchemaHelper {
15
15
  preprocessBindings(queryParams) {
16
16
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
17
17
  }
18
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
19
- if (hasMultiRelationalSort) {
18
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
19
+ if (hasRelationalSort) {
20
20
  /*
21
21
  Postgres only requires selected columns that are not functionally dependent on the primary key to be
22
22
  included in the GROUP BY clause. Since the results are already grouped by the primary key, we don't need to
23
- always include the sort columns in the GROUP BY but only if there is a multi relational sort involved, eg.
23
+ always include the sort columns in the GROUP BY but only if there is a relational sort involved, eg.
24
24
  a sort column that comes from a related M2O relation.
25
25
 
26
26
  > When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list
@@ -36,5 +36,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
36
36
  */
37
37
  getDatabaseSize(): Promise<number | null>;
38
38
  preprocessBindings(queryParams: Sql): Sql;
39
- addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
39
+ addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
40
40
  }
@@ -97,7 +97,7 @@ export class SchemaHelper extends DatabaseHelper {
97
97
  preprocessBindings(queryParams) {
98
98
  return queryParams;
99
99
  }
100
- addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasMultiRelationalSort) {
100
+ addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasRelationalSort) {
101
101
  // no-op by default
102
102
  }
103
103
  }
@@ -1,6 +1,3 @@
1
1
  import type { Knex } from 'knex';
2
- import type { Permission } from '@directus/types';
3
- export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): any[];
4
- export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
5
2
  export declare function up(knex: Knex): Promise<void>;
6
3
  export declare function down(knex: Knex): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { processChunk, toBoolean } from '@directus/utils';
2
- import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
2
+ import { omit } from 'lodash-es';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { useLogger } from '../../logger/index.js';
5
5
  import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
@@ -7,98 +7,7 @@ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
7
7
  import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
8
8
  import { getSchema } from '../../utils/get-schema.js';
9
9
  import { getSchemaInspector } from '../index.js';
10
- // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
11
- export function mergePermissions(strategy, ...permissions) {
12
- const allPermissions = flatten(permissions);
13
- const mergedPermissions = allPermissions
14
- .reduce((acc, val) => {
15
- const key = `${val.collection}__${val.action}`;
16
- const current = acc.get(key);
17
- acc.set(key, current ? mergePermission(strategy, current, val) : val);
18
- return acc;
19
- }, new Map())
20
- .values();
21
- return Array.from(mergedPermissions);
22
- }
23
- export function mergePermission(strategy, currentPerm, newPerm) {
24
- const logicalKey = `_${strategy}`;
25
- let { permissions, validation, fields, presets } = currentPerm;
26
- if (newPerm.permissions) {
27
- if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
28
- permissions = {
29
- [logicalKey]: [
30
- ...currentPerm.permissions[logicalKey],
31
- newPerm.permissions,
32
- ],
33
- };
34
- }
35
- else if (currentPerm.permissions) {
36
- // Empty {} supersedes other permissions in _OR merge
37
- if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
38
- permissions = {};
39
- }
40
- else {
41
- permissions = {
42
- [logicalKey]: [currentPerm.permissions, newPerm.permissions],
43
- };
44
- }
45
- }
46
- else {
47
- permissions = {
48
- [logicalKey]: [newPerm.permissions],
49
- };
50
- }
51
- }
52
- if (newPerm.validation) {
53
- if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
54
- validation = {
55
- [logicalKey]: [
56
- ...currentPerm.validation[logicalKey],
57
- newPerm.validation,
58
- ],
59
- };
60
- }
61
- else if (currentPerm.validation) {
62
- // Empty {} supersedes other validations in _OR merge
63
- if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
64
- validation = {};
65
- }
66
- else {
67
- validation = {
68
- [logicalKey]: [currentPerm.validation, newPerm.validation],
69
- };
70
- }
71
- }
72
- else {
73
- validation = {
74
- [logicalKey]: [newPerm.validation],
75
- };
76
- }
77
- }
78
- if (newPerm.fields) {
79
- if (Array.isArray(currentPerm.fields) && strategy === 'or') {
80
- fields = uniq([...currentPerm.fields, ...newPerm.fields]);
81
- }
82
- else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
83
- fields = intersection(currentPerm.fields, newPerm.fields);
84
- }
85
- else {
86
- fields = newPerm.fields;
87
- }
88
- if (fields.includes('*'))
89
- fields = ['*'];
90
- }
91
- if (newPerm.presets) {
92
- presets = merge({}, presets, newPerm.presets);
93
- }
94
- return omit({
95
- ...currentPerm,
96
- permissions,
97
- validation,
98
- fields,
99
- presets,
100
- }, ['id', 'system']);
101
- }
10
+ import { mergePermissions } from '../../permissions/utils/merge-permissions.js';
102
11
  async function fetchRoleAccess(roles, context) {
103
12
  const roleAccess = {
104
13
  admin_access: false,
@@ -286,7 +195,12 @@ export async function down(knex) {
286
195
  const policies = await fetchPolicies({ roles: roleTree, user: null, ip: null }, context);
287
196
  // fetch all of the policies permissions
288
197
  const rawPermissions = await fetchPermissions({
289
- accountability: { role: null, roles: roleTree, user: null, app: roleAccess?.app_access || false },
198
+ accountability: {
199
+ role: null,
200
+ roles: roleTree,
201
+ user: null,
202
+ app: roleAccess?.app_access || false,
203
+ },
290
204
  policies,
291
205
  bypassDynamicVariableProcessing: true,
292
206
  }, context);
@@ -181,7 +181,7 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
181
181
  // Since the fields are expected to be the same for a single primary key it is safe to include them in the
182
182
  // group by without influencing the result.
183
183
  // This inclusion depends on the DB vendor, as such it is handled in a dialect specific helper.
184
- helpers.schema.addInnerSortFieldsToGroupBy(groupByFields, innerQuerySortRecords, hasMultiRelationalSort ?? false);
184
+ helpers.schema.addInnerSortFieldsToGroupBy(groupByFields, innerQuerySortRecords, (hasMultiRelationalSort || sortRecords?.some(({ column }) => column.includes('.'))) ?? false);
185
185
  dbQuery.groupBy(groupByFields);
186
186
  }
187
187
  const wrapperQuery = knex
@@ -16,7 +16,7 @@ export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, ali
16
16
  sqlParts.push(val);
17
17
  }
18
18
  }
19
- const sql = sqlParts.join(' ');
19
+ const sql = sqlParts.length > 0 ? sqlParts.join(' ') : '1';
20
20
  const bindings = [...caseQuery.toSQL().bindings, column];
21
21
  let rawCase = `(CASE WHEN ${sql} THEN ?? END)`;
22
22
  if (alias) {
@@ -4,7 +4,7 @@ export interface FetchPermissionsOptions {
4
4
  action?: PermissionsAction;
5
5
  policies: string[];
6
6
  collections?: string[];
7
- accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
7
+ accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app' | 'share' | 'ip'>;
8
8
  bypassDynamicVariableProcessing?: boolean;
9
9
  }
10
10
  export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<{
@@ -1,6 +1,7 @@
1
1
  import { fetchDynamicVariableContext } from '../utils/fetch-dynamic-variable-context.js';
2
2
  import { fetchRawPermissions } from '../utils/fetch-raw-permissions.js';
3
3
  import { processPermissions } from '../utils/process-permissions.js';
4
+ import { getPermissionsForShare } from '../utils/get-permissions-for-share.js';
4
5
  export async function fetchPermissions(options, context) {
5
6
  const permissions = await fetchRawPermissions({ ...options, bypassMinimalAppPermissions: options.bypassDynamicVariableProcessing ?? false }, context);
6
7
  if (options.accountability && !options.bypassDynamicVariableProcessing) {
@@ -15,7 +16,9 @@ export async function fetchPermissions(options, context) {
15
16
  accountability: options.accountability,
16
17
  permissionsContext,
17
18
  });
18
- // TODO merge in permissions coming from the share scope
19
+ if (options.accountability.share && (options.action === undefined || options.action === 'read')) {
20
+ return await getPermissionsForShare(options.accountability, options.collections, context);
21
+ }
19
22
  return processedPermissions;
20
23
  }
21
24
  return permissions;
@@ -6,6 +6,7 @@ export interface ValidateAccessOptions {
6
6
  collection: string;
7
7
  primaryKeys?: PrimaryKey[];
8
8
  fields?: string[];
9
+ skipCollectionExistsCheck?: boolean;
9
10
  }
10
11
  /**
11
12
  * Validate if the current user has access to perform action against the given collection and
@@ -8,7 +8,7 @@ import { validateItemAccess } from './lib/validate-item-access.js';
8
8
  */
9
9
  export async function validateAccess(options, context) {
10
10
  // Skip further validation if the collection does not exist
11
- if (options.collection in context.schema.collections === false) {
11
+ if (!options.skipCollectionExistsCheck && options.collection in context.schema.collections === false) {
12
12
  throw new ForbiddenError({
13
13
  reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
14
14
  });
@@ -0,0 +1,12 @@
1
+ import type { AbstractServiceOptions } from '../../types/services.js';
2
+ export interface ShareInfo {
3
+ collection: string;
4
+ item: string;
5
+ role: string | null;
6
+ user_created: {
7
+ id: string;
8
+ role: string;
9
+ };
10
+ }
11
+ export declare const fetchShareInfo: typeof _fetchShareInfo;
12
+ export declare function _fetchShareInfo(shareId: string, context: AbstractServiceOptions): Promise<ShareInfo>;
@@ -0,0 +1,9 @@
1
+ import { withCache } from './with-cache.js';
2
+ export const fetchShareInfo = withCache('share-info', _fetchShareInfo);
3
+ export async function _fetchShareInfo(shareId, context) {
4
+ const { SharesService } = await import('../../services/shares.js');
5
+ const sharesService = new SharesService(context);
6
+ return (await sharesService.readOne(shareId, {
7
+ fields: ['collection', 'item', 'role', 'user_created.id', 'user_created.role'],
8
+ }));
9
+ }
@@ -0,0 +1,4 @@
1
+ import type { Accountability, Permission, SchemaOverview } from '@directus/types';
2
+ import type { Context } from '../types.js';
3
+ export declare function getPermissionsForShare(accountability: Pick<Accountability, 'share' | 'ip'>, collections: string[] | undefined, context: Context): Promise<Permission[]>;
4
+ export declare function traverse(schema: SchemaOverview, rootItemPrimaryKeyField: string, rootItemPrimaryKey: string, currentCollection: string, parentCollections?: string[], path?: string[]): Partial<Permission>[];
@@ -0,0 +1,182 @@
1
+ import { schemaPermissions } from '@directus/system-data';
2
+ import { set, uniq } from 'lodash-es';
3
+ import { fetchAllowedFieldMap } from '../modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
4
+ import { fetchShareInfo } from './fetch-share-info.js';
5
+ import { mergePermissions } from './merge-permissions.js';
6
+ import { fetchPermissions } from '../lib/fetch-permissions.js';
7
+ import { fetchPolicies } from '../lib/fetch-policies.js';
8
+ import { fetchRolesTree } from '../lib/fetch-roles-tree.js';
9
+ import { reduceSchema } from '../../utils/reduce-schema.js';
10
+ import { fetchGlobalAccess } from '../modules/fetch-global-access/fetch-global-access.js';
11
+ export async function getPermissionsForShare(accountability, collections, context) {
12
+ const defaults = {
13
+ action: 'read',
14
+ collection: '',
15
+ permissions: {},
16
+ policy: null,
17
+ validation: null,
18
+ presets: null,
19
+ fields: null,
20
+ };
21
+ const { collection, item, role, user_created } = await fetchShareInfo(accountability.share, context);
22
+ const userAccountability = {
23
+ user: user_created.id,
24
+ role: user_created.role,
25
+ roles: await fetchRolesTree(user_created.role, context.knex),
26
+ admin: false,
27
+ app: false,
28
+ ip: accountability.ip,
29
+ };
30
+ // Fallback to public accountability so merging later on has no issues
31
+ const shareAccountability = {
32
+ user: null,
33
+ role: role,
34
+ roles: await fetchRolesTree(role, context.knex),
35
+ admin: false,
36
+ app: false,
37
+ ip: accountability.ip,
38
+ };
39
+ const [{ admin: shareIsAdmin }, { admin: userIsAdmin }, userPermissions, sharePermissions, shareFieldMap, userFieldMap,] = await Promise.all([
40
+ fetchGlobalAccess(shareAccountability, context.knex),
41
+ fetchGlobalAccess(userAccountability, context.knex),
42
+ getPermissionsForAccountability(userAccountability, context),
43
+ getPermissionsForAccountability(shareAccountability, context),
44
+ fetchAllowedFieldMap({
45
+ accountability: shareAccountability,
46
+ action: 'read',
47
+ }, context),
48
+ fetchAllowedFieldMap({
49
+ accountability: userAccountability,
50
+ action: 'read',
51
+ }, context),
52
+ ]);
53
+ const isAdmin = userIsAdmin && shareIsAdmin;
54
+ let permissions = [];
55
+ let reducedSchema;
56
+ if (isAdmin) {
57
+ defaults.fields = ['*'];
58
+ reducedSchema = context.schema;
59
+ }
60
+ else if (userIsAdmin && !shareIsAdmin) {
61
+ permissions = sharePermissions;
62
+ reducedSchema = reduceSchema(context.schema, shareFieldMap);
63
+ }
64
+ else if (shareIsAdmin && !userIsAdmin) {
65
+ permissions = userPermissions;
66
+ reducedSchema = reduceSchema(context.schema, userFieldMap);
67
+ }
68
+ else {
69
+ permissions = mergePermissions('intersection', sharePermissions, userPermissions);
70
+ reducedSchema = reduceSchema(context.schema, shareFieldMap);
71
+ reducedSchema = reduceSchema(reducedSchema, userFieldMap);
72
+ }
73
+ const parentPrimaryKeyField = context.schema.collections[collection].primary;
74
+ const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection);
75
+ const parentCollectionPermission = {
76
+ ...defaults,
77
+ collection,
78
+ permissions: {
79
+ [parentPrimaryKeyField]: {
80
+ _eq: item,
81
+ },
82
+ },
83
+ };
84
+ // All permissions that will be merged into the original permissions set
85
+ const allGeneratedPermissions = [
86
+ parentCollectionPermission,
87
+ ...relationalPermissions.map((generated) => ({ ...defaults, ...generated })),
88
+ ...schemaPermissions,
89
+ ];
90
+ // All the collections that are touched through the relational tree from the current root collection, and the schema collections
91
+ const allowedCollections = uniq(allGeneratedPermissions.map(({ collection }) => collection));
92
+ const generatedPermissions = [];
93
+ // Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve)
94
+ // the items of a collection if you entered that collection from multiple angles
95
+ for (const collection of allowedCollections) {
96
+ const permissionsForCollection = allGeneratedPermissions.filter((permission) => permission.collection === collection);
97
+ if (permissionsForCollection.length > 0) {
98
+ generatedPermissions.push(...mergePermissions('or', permissionsForCollection));
99
+ }
100
+ else {
101
+ generatedPermissions.push(...permissionsForCollection);
102
+ }
103
+ }
104
+ if (isAdmin) {
105
+ return filterCollections(collections, generatedPermissions);
106
+ }
107
+ // Explicitly filter out permissions to collections unrelated to the root parent item.
108
+ const limitedPermissions = permissions.filter(({ action, collection }) => allowedCollections.includes(collection) && action === 'read');
109
+ return filterCollections(collections, mergePermissions('and', limitedPermissions, generatedPermissions));
110
+ }
111
+ function filterCollections(collections, permissions) {
112
+ if (!collections) {
113
+ return permissions;
114
+ }
115
+ return permissions.filter(({ collection }) => collections.includes(collection));
116
+ }
117
+ async function getPermissionsForAccountability(accountability, context) {
118
+ const policies = await fetchPolicies(accountability, context);
119
+ return fetchPermissions({
120
+ policies,
121
+ accountability,
122
+ }, context);
123
+ }
124
+ export function traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, currentCollection, parentCollections = [], path = []) {
125
+ const permissions = [];
126
+ // If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit.
127
+ // This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or
128
+ // articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁
129
+ if (parentCollections.includes(currentCollection)) {
130
+ return permissions;
131
+ }
132
+ const relationsInCollection = schema.relations.filter((relation) => {
133
+ return relation.collection === currentCollection || relation.related_collection === currentCollection;
134
+ });
135
+ for (const relation of relationsInCollection) {
136
+ let type;
137
+ if (relation.related_collection === currentCollection) {
138
+ type = 'o2m';
139
+ }
140
+ else if (!relation.related_collection) {
141
+ type = 'a2o';
142
+ }
143
+ else {
144
+ type = 'm2o';
145
+ }
146
+ if (type === 'o2m') {
147
+ permissions.push({
148
+ collection: relation.collection,
149
+ permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey),
150
+ });
151
+ permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.collection, [...parentCollections, currentCollection], [...path, relation.field]));
152
+ }
153
+ if (type === 'a2o' && relation.meta?.one_allowed_collections) {
154
+ for (const collection of relation.meta.one_allowed_collections) {
155
+ permissions.push({
156
+ collection,
157
+ permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
158
+ });
159
+ }
160
+ }
161
+ if (type === 'm2o') {
162
+ permissions.push({
163
+ collection: relation.related_collection,
164
+ permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
165
+ });
166
+ if (relation.meta?.one_field) {
167
+ permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.related_collection, [...parentCollections, currentCollection], [...path, relation.meta?.one_field]));
168
+ }
169
+ }
170
+ }
171
+ return permissions;
172
+ }
173
+ function getFilterForPath(type, path, rootPrimaryKeyField, rootPrimaryKey) {
174
+ const filter = {};
175
+ if (type === 'm2o' || type === 'a2o') {
176
+ set(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } });
177
+ }
178
+ else {
179
+ set(filter, path.reverse(), { _eq: rootPrimaryKey });
180
+ }
181
+ return filter;
182
+ }
@@ -0,0 +1,9 @@
1
+ import type { Permission } from '@directus/types';
2
+ /**
3
+ * Merges multiple permission lists into a flat list of unique permissions.
4
+ * @param strategy `and` or `or` deduplicate permissions while `intersection` makes sure only common permissions across all lists are kept and overlapping permissions are merged through `and`.
5
+ * @param permissions List of permission lists to merge.
6
+ * @returns A flat list of unique permissions.
7
+ */
8
+ export declare function mergePermissions(strategy: 'and' | 'or' | 'intersection', ...permissions: Permission[][]): Permission[];
9
+ export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
@@ -0,0 +1,118 @@
1
+ import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
2
+ // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
3
+ /**
4
+ * Merges multiple permission lists into a flat list of unique permissions.
5
+ * @param strategy `and` or `or` deduplicate permissions while `intersection` makes sure only common permissions across all lists are kept and overlapping permissions are merged through `and`.
6
+ * @param permissions List of permission lists to merge.
7
+ * @returns A flat list of unique permissions.
8
+ */
9
+ export function mergePermissions(strategy, ...permissions) {
10
+ let allPermissions;
11
+ // Only keep permissions that are common to all lists
12
+ if (strategy === 'intersection') {
13
+ const permissionKeys = permissions.map((permissions) => {
14
+ return new Set(permissions.map((permission) => `${permission.collection}__${permission.action}`));
15
+ });
16
+ const intersectionKeys = permissionKeys.reduce((acc, val) => {
17
+ return new Set([...acc].filter((x) => val.has(x)));
18
+ }, permissionKeys[0]);
19
+ const deduplicateSubpermissions = permissions.map((permissions) => {
20
+ return mergePermissions('or', permissions);
21
+ });
22
+ allPermissions = flatten(deduplicateSubpermissions).filter((permission) => {
23
+ return intersectionKeys.has(`${permission.collection}__${permission.action}`);
24
+ });
25
+ strategy = 'and';
26
+ }
27
+ else {
28
+ allPermissions = flatten(permissions);
29
+ }
30
+ const mergedPermissions = allPermissions
31
+ .reduce((acc, val) => {
32
+ const key = `${val.collection}__${val.action}`;
33
+ const current = acc.get(key);
34
+ acc.set(key, current ? mergePermission(strategy, current, val) : val);
35
+ return acc;
36
+ }, new Map())
37
+ .values();
38
+ return Array.from(mergedPermissions);
39
+ }
40
+ export function mergePermission(strategy, currentPerm, newPerm) {
41
+ const logicalKey = `_${strategy}`;
42
+ let { permissions, validation, fields, presets } = currentPerm;
43
+ if (newPerm.permissions) {
44
+ if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
45
+ permissions = {
46
+ [logicalKey]: [
47
+ ...currentPerm.permissions[logicalKey],
48
+ newPerm.permissions,
49
+ ],
50
+ };
51
+ }
52
+ else if (currentPerm.permissions) {
53
+ // Empty {} supersedes other permissions in _OR merge
54
+ if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
55
+ permissions = {};
56
+ }
57
+ else {
58
+ permissions = {
59
+ [logicalKey]: [currentPerm.permissions, newPerm.permissions],
60
+ };
61
+ }
62
+ }
63
+ else {
64
+ permissions = {
65
+ [logicalKey]: [newPerm.permissions],
66
+ };
67
+ }
68
+ }
69
+ if (newPerm.validation) {
70
+ if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
71
+ validation = {
72
+ [logicalKey]: [
73
+ ...currentPerm.validation[logicalKey],
74
+ newPerm.validation,
75
+ ],
76
+ };
77
+ }
78
+ else if (currentPerm.validation) {
79
+ // Empty {} supersedes other validations in _OR merge
80
+ if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
81
+ validation = {};
82
+ }
83
+ else {
84
+ validation = {
85
+ [logicalKey]: [currentPerm.validation, newPerm.validation],
86
+ };
87
+ }
88
+ }
89
+ else {
90
+ validation = {
91
+ [logicalKey]: [newPerm.validation],
92
+ };
93
+ }
94
+ }
95
+ if (newPerm.fields) {
96
+ if (Array.isArray(currentPerm.fields) && strategy === 'or') {
97
+ fields = uniq([...currentPerm.fields, ...newPerm.fields]);
98
+ }
99
+ else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
100
+ fields = intersection(currentPerm.fields, newPerm.fields);
101
+ }
102
+ else {
103
+ fields = newPerm.fields;
104
+ }
105
+ if (fields.includes('*'))
106
+ fields = ['*'];
107
+ }
108
+ if (newPerm.presets) {
109
+ presets = merge({}, presets, newPerm.presets);
110
+ }
111
+ return omit({
112
+ ...currentPerm,
113
+ permissions,
114
+ validation,
115
+ fields,
116
+ presets,
117
+ }, ['id', 'system']);
118
+ }
@@ -121,7 +121,7 @@ export class AssetsService {
121
121
  reason: 'Server too busy',
122
122
  });
123
123
  }
124
- const version = file.modified_on !== undefined ? String(new Date(file.modified_on).getTime() / 1000) : undefined;
124
+ const version = file.modified_on !== undefined ? String(Math.round(new Date(file.modified_on).getTime() / 1000)) : undefined;
125
125
  const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
126
126
  const transformer = getSharpInstance();
127
127
  transformer.timeout({
@@ -212,13 +212,8 @@ export class AuthenticationService {
212
212
  user_auth_data: 'u.auth_data',
213
213
  user_role: 'u.role',
214
214
  share_id: 'd.id',
215
- share_item: 'd.item',
216
- share_role: 'd.role',
217
- share_collection: 'd.collection',
218
215
  share_start: 'd.date_start',
219
216
  share_end: 'd.date_end',
220
- share_times_used: 'd.times_used',
221
- share_max_uses: 'd.max_uses',
222
217
  })
223
218
  .from('directus_sessions AS s')
224
219
  .leftJoin('directus_users AS u', 's.user', 'u.id')
@@ -289,11 +284,7 @@ export class AuthenticationService {
289
284
  }
290
285
  if (record.share_id) {
291
286
  tokenPayload.share = record.share_id;
292
- tokenPayload.role = record.share_role;
293
- tokenPayload.share_scope = {
294
- collection: record.share_collection,
295
- item: record.share_item,
296
- };
287
+ tokenPayload.role = null;
297
288
  tokenPayload.app_access = false;
298
289
  tokenPayload.admin_access = false;
299
290
  delete tokenPayload.id;
@@ -155,11 +155,10 @@ export class CollectionsService {
155
155
  if (opts?.autoPurgeSystemCache !== false) {
156
156
  await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
157
157
  }
158
- // Refresh the schema for subsequent reads
159
- this.schema = await getSchema();
160
158
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
159
+ const updatedSchema = await getSchema();
161
160
  for (const nestedActionEvent of nestedActionEvents) {
162
- nestedActionEvent.context.schema = this.schema;
161
+ nestedActionEvent.context.schema = updatedSchema;
163
162
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
164
163
  }
165
164
  }
@@ -197,11 +196,10 @@ export class CollectionsService {
197
196
  if (opts?.autoPurgeSystemCache !== false) {
198
197
  await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
199
198
  }
200
- // Refresh the schema for subsequent reads
201
- this.schema = await getSchema();
202
199
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
200
+ const updatedSchema = await getSchema();
203
201
  for (const nestedActionEvent of nestedActionEvents) {
204
- nestedActionEvent.context.schema = this.schema;
202
+ nestedActionEvent.context.schema = updatedSchema;
205
203
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
206
204
  }
207
205
  }
@@ -290,6 +288,7 @@ export class CollectionsService {
290
288
  accountability: this.accountability,
291
289
  action: 'read',
292
290
  collection,
291
+ skipCollectionExistsCheck: true,
293
292
  }, {
294
293
  schema: this.schema,
295
294
  knex: this.knex,
@@ -5,6 +5,7 @@ import { cloneDeep, mergeWith, uniq } from 'lodash-es';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { useLogger } from '../logger/index.js';
7
7
  import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
8
+ import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
8
9
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
9
10
  import { isValidUuid } from '../utils/is-valid-uuid.js';
10
11
  import { transaction } from '../utils/transaction.js';
@@ -118,16 +119,19 @@ export class CommentsService extends ItemsService {
118
119
  for (const mention of mentions) {
119
120
  const userID = mention.substring(1);
120
121
  const user = await this.usersService.readOne(userID, {
121
- fields: ['id', 'first_name', 'last_name', 'email', 'role.id', 'role.admin_access', 'role.app_access'],
122
+ fields: ['id', 'first_name', 'last_name', 'email', 'role.id'],
122
123
  });
123
124
  const accountability = {
124
125
  user: userID,
125
126
  role: user['role']?.id ?? null,
126
- admin: user['role']?.admin_access ?? null,
127
- app: user['role']?.app_access ?? null,
128
- roles: await fetchRolesTree(user['role']?.id, this.knex),
127
+ admin: false,
128
+ app: false,
129
+ roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
129
130
  ip: null,
130
131
  };
132
+ const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
133
+ accountability.admin = userGlobalAccess.admin;
134
+ accountability.app = userGlobalAccess.app;
131
135
  const usersService = new UsersService({ schema: this.schema, accountability });
132
136
  try {
133
137
  await validateAccess({
@@ -4,6 +4,8 @@ import { ItemsService } from './items.js';
4
4
  export declare class SharesService extends ItemsService {
5
5
  constructor(options: AbstractServiceOptions);
6
6
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
7
+ updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
8
+ deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
7
9
  login(payload: Record<string, any>, options?: Partial<{
8
10
  session: boolean;
9
11
  }>): Promise<Omit<LoginResult, 'id'>>;
@@ -2,6 +2,7 @@ import { useEnv } from '@directus/env';
2
2
  import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
3
3
  import argon2 from 'argon2';
4
4
  import jwt from 'jsonwebtoken';
5
+ import { nanoid } from 'nanoid';
5
6
  import { useLogger } from '../logger/index.js';
6
7
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
7
8
  import { getMilliseconds } from '../utils/get-milliseconds.js';
@@ -12,6 +13,7 @@ import { userName } from '../utils/user-name.js';
12
13
  import { ItemsService } from './items.js';
13
14
  import { MailService } from './mail/index.js';
14
15
  import { UsersService } from './users.js';
16
+ import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
15
17
  const env = useEnv();
16
18
  const logger = useLogger();
17
19
  export class SharesService extends ItemsService {
@@ -32,14 +34,18 @@ export class SharesService extends ItemsService {
32
34
  }
33
35
  return super.createOne(data, opts);
34
36
  }
37
+ async updateMany(keys, data, opts) {
38
+ await clearPermissionsCache();
39
+ return super.updateMany(keys, data, opts);
40
+ }
41
+ async deleteMany(keys, opts) {
42
+ await clearPermissionsCache();
43
+ return super.deleteMany(keys, opts);
44
+ }
35
45
  async login(payload, options) {
36
- const { nanoid } = await import('nanoid');
37
46
  const record = await this.knex
38
47
  .select({
39
48
  share_id: 'id',
40
- share_role: 'role',
41
- share_item: 'item',
42
- share_collection: 'collection',
43
49
  share_start: 'date_start',
44
50
  share_end: 'date_end',
45
51
  share_times_used: 'times_used',
@@ -70,12 +76,8 @@ export class SharesService extends ItemsService {
70
76
  const tokenPayload = {
71
77
  app_access: false,
72
78
  admin_access: false,
73
- role: record.share_role,
79
+ role: null,
74
80
  share: record.share_id,
75
- share_scope: {
76
- item: record.share_item,
77
- collection: record.share_collection,
78
- },
79
81
  };
80
82
  const refreshToken = nanoid(64);
81
83
  const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
@@ -31,16 +31,9 @@ export type DirectusTokenPayload = {
31
31
  app_access: boolean | number;
32
32
  admin_access: boolean | number;
33
33
  share?: string;
34
- share_scope?: {
35
- collection: string;
36
- item: string;
37
- };
38
34
  };
39
35
  export type ShareData = {
40
36
  share_id: string;
41
- share_role: string;
42
- share_item: string;
43
- share_collection: string;
44
37
  share_start: Date;
45
38
  share_end: Date;
46
39
  share_times_used: number;
@@ -21,8 +21,6 @@ export async function getAccountabilityForToken(token, accountability) {
21
21
  }
22
22
  if (payload.share)
23
23
  accountability.share = payload.share;
24
- if (payload.share_scope)
25
- accountability.share_scope = payload.share_scope;
26
24
  if (payload.id)
27
25
  accountability.user = payload.id;
28
26
  accountability.role = payload.role;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "23.1.0",
3
+ "version": "23.1.2",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -149,29 +149,29 @@
149
149
  "ws": "8.18.0",
150
150
  "zod": "3.23.8",
151
151
  "zod-validation-error": "3.4.0",
152
+ "@directus/app": "13.3.2",
152
153
  "@directus/constants": "12.0.0",
154
+ "@directus/env": "3.1.3",
153
155
  "@directus/errors": "1.0.1",
154
- "@directus/app": "13.3.0",
155
- "@directus/env": "3.1.2",
156
- "@directus/extensions-registry": "2.0.3",
157
- "@directus/extensions-sdk": "12.1.1",
158
- "@directus/extensions": "2.0.3",
156
+ "@directus/extensions-registry": "2.0.4",
157
+ "@directus/extensions": "2.0.4",
158
+ "@directus/extensions-sdk": "12.1.2",
159
159
  "@directus/format-title": "11.0.0",
160
- "@directus/memory": "2.0.3",
161
- "@directus/pressure": "2.0.2",
162
- "@directus/specs": "11.1.0",
160
+ "@directus/memory": "2.0.4",
161
+ "@directus/pressure": "2.0.3",
163
162
  "@directus/schema": "12.1.1",
163
+ "@directus/specs": "11.1.0",
164
164
  "@directus/storage": "11.0.1",
165
- "@directus/storage-driver-azure": "11.0.2",
166
- "@directus/storage-driver-cloudinary": "11.0.3",
167
- "@directus/storage-driver-gcs": "11.0.2",
165
+ "@directus/storage-driver-azure": "11.1.0",
166
+ "@directus/storage-driver-gcs": "11.1.0",
167
+ "@directus/storage-driver-cloudinary": "11.1.0",
168
168
  "@directus/storage-driver-local": "11.0.1",
169
- "@directus/storage-driver-s3": "11.0.2",
170
- "@directus/storage-driver-supabase": "2.0.2",
171
- "@directus/system-data": "2.1.0",
172
- "@directus/utils": "12.0.2",
173
- "directus": "11.1.2",
174
- "@directus/validation": "1.0.2"
169
+ "@directus/storage-driver-s3": "11.0.3",
170
+ "@directus/storage-driver-supabase": "2.1.0",
171
+ "@directus/system-data": "2.1.1",
172
+ "@directus/validation": "1.0.3",
173
+ "@directus/utils": "12.0.3",
174
+ "directus": "11.2.1"
175
175
  },
176
176
  "devDependencies": {
177
177
  "@ngneat/falso": "7.2.0",
@@ -214,8 +214,8 @@
214
214
  "typescript": "5.6.3",
215
215
  "vitest": "2.1.2",
216
216
  "@directus/random": "1.0.0",
217
- "@directus/types": "12.2.0",
218
- "@directus/tsconfig": "2.0.0"
217
+ "@directus/tsconfig": "2.0.0",
218
+ "@directus/types": "12.2.1"
219
219
  },
220
220
  "optionalDependencies": {
221
221
  "@keyv/redis": "3.0.1",