@directus/api 23.0.0 → 23.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/controllers/activity.js +30 -27
  3. package/dist/controllers/assets.js +1 -1
  4. package/dist/controllers/comments.d.ts +2 -0
  5. package/dist/controllers/comments.js +153 -0
  6. package/dist/controllers/permissions.js +1 -1
  7. package/dist/controllers/users.js +4 -8
  8. package/dist/controllers/versions.js +10 -5
  9. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -2
  11. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/mssql.js +1 -1
  13. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -1
  14. package/dist/database/helpers/schema/dialects/mysql.js +2 -2
  15. package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -1
  16. package/dist/database/helpers/schema/dialects/oracle.js +1 -1
  17. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  18. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  19. package/dist/database/helpers/schema/types.d.ts +1 -1
  20. package/dist/database/helpers/schema/types.js +1 -1
  21. package/dist/database/index.js +3 -0
  22. package/dist/database/migrations/20240806A-permissions-policies.d.ts +0 -3
  23. package/dist/database/migrations/20240806A-permissions-policies.js +8 -94
  24. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  25. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  26. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  27. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  28. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  29. package/dist/database/run-ast/lib/get-db-query.js +3 -3
  30. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  31. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  32. package/dist/database/run-ast/run-ast.js +8 -1
  33. package/dist/database/run-ast/utils/apply-case-when.js +1 -1
  34. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  35. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  36. package/dist/permissions/lib/fetch-permissions.d.ts +1 -1
  37. package/dist/permissions/lib/fetch-permissions.js +4 -1
  38. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  39. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  40. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  41. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  42. package/dist/permissions/utils/fetch-share-info.d.ts +12 -0
  43. package/dist/permissions/utils/fetch-share-info.js +9 -0
  44. package/dist/permissions/utils/get-permissions-for-share.d.ts +4 -0
  45. package/dist/permissions/utils/get-permissions-for-share.js +182 -0
  46. package/dist/permissions/utils/merge-permissions.d.ts +9 -0
  47. package/dist/permissions/utils/merge-permissions.js +118 -0
  48. package/dist/services/activity.d.ts +1 -7
  49. package/dist/services/activity.js +0 -103
  50. package/dist/services/assets.js +5 -4
  51. package/dist/services/authentication.js +1 -10
  52. package/dist/services/collections.js +6 -4
  53. package/dist/services/comments.d.ts +31 -0
  54. package/dist/services/comments.js +378 -0
  55. package/dist/services/graphql/index.js +17 -16
  56. package/dist/services/index.d.ts +1 -0
  57. package/dist/services/index.js +1 -0
  58. package/dist/services/items.js +3 -1
  59. package/dist/services/mail/index.d.ts +2 -1
  60. package/dist/services/mail/index.js +4 -1
  61. package/dist/services/payload.js +15 -14
  62. package/dist/services/shares.d.ts +2 -0
  63. package/dist/services/shares.js +11 -9
  64. package/dist/services/users.js +1 -0
  65. package/dist/services/versions.js +59 -44
  66. package/dist/types/auth.d.ts +0 -7
  67. package/dist/utils/apply-diff.js +5 -6
  68. package/dist/utils/get-accountability-for-token.js +0 -2
  69. package/dist/utils/get-service.js +3 -1
  70. package/dist/utils/sanitize-schema.d.ts +1 -1
  71. package/dist/utils/sanitize-schema.js +2 -0
  72. package/package.json +52 -52
@@ -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
+ }
@@ -1,11 +1,5 @@
1
- import type { Item, PrimaryKey } from '@directus/types';
2
- import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
1
+ import type { AbstractServiceOptions } from '../types/index.js';
3
2
  import { ItemsService } from './items.js';
4
- import { NotificationsService } from './notifications.js';
5
- import { UsersService } from './users.js';
6
3
  export declare class ActivityService extends ItemsService {
7
- notificationsService: NotificationsService;
8
- usersService: UsersService;
9
4
  constructor(options: AbstractServiceOptions);
10
- createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
11
5
  }
@@ -1,109 +1,6 @@
1
- import { Action } from '@directus/constants';
2
- import { useEnv } from '@directus/env';
3
- import { ErrorCode, isDirectusError } from '@directus/errors';
4
- import { uniq } from 'lodash-es';
5
- import { useLogger } from '../logger/index.js';
6
- import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
7
- import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
8
- import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
9
- import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
10
- import { isValidUuid } from '../utils/is-valid-uuid.js';
11
- import { Url } from '../utils/url.js';
12
- import { userName } from '../utils/user-name.js';
13
1
  import { ItemsService } from './items.js';
14
- import { NotificationsService } from './notifications.js';
15
- import { UsersService } from './users.js';
16
- const env = useEnv();
17
- const logger = useLogger();
18
2
  export class ActivityService extends ItemsService {
19
- notificationsService;
20
- usersService;
21
3
  constructor(options) {
22
4
  super('directus_activity', options);
23
- this.notificationsService = new NotificationsService({ schema: this.schema });
24
- this.usersService = new UsersService({ schema: this.schema });
25
- }
26
- async createOne(data, opts) {
27
- if (data['action'] === Action.COMMENT && typeof data['comment'] === 'string') {
28
- const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
29
- const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
30
- const sender = await this.usersService.readOne(this.accountability.user, {
31
- fields: ['id', 'first_name', 'last_name', 'email'],
32
- });
33
- for (const mention of mentions) {
34
- const userID = mention.substring(1);
35
- const user = await this.usersService.readOne(userID, {
36
- fields: ['id', 'first_name', 'last_name', 'email', 'role'],
37
- });
38
- const roles = await fetchRolesTree(user['role'], this.knex);
39
- const globalAccess = await fetchGlobalAccess({ user: user['id'], roles, ip: null }, this.knex);
40
- const accountability = createDefaultAccountability({
41
- user: userID,
42
- role: user['role']?.id ?? null,
43
- roles,
44
- ...globalAccess,
45
- });
46
- const usersService = new UsersService({ schema: this.schema, accountability });
47
- try {
48
- if (this.accountability) {
49
- await validateAccess({
50
- accountability: this.accountability,
51
- action: 'read',
52
- collection: data['collection'],
53
- primaryKeys: [data['item']],
54
- }, {
55
- knex: this.knex,
56
- schema: this.schema,
57
- });
58
- }
59
- const templateData = await usersService.readByQuery({
60
- fields: ['id', 'first_name', 'last_name', 'email'],
61
- filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
62
- });
63
- const userPreviews = templateData.reduce((acc, user) => {
64
- acc[user['id']] = `<em>${userName(user)}</em>`;
65
- return acc;
66
- }, {});
67
- let comment = data['comment'];
68
- for (const mention of mentions) {
69
- const uuid = mention.substring(1);
70
- // We only match on UUIDs in the first place. This is just an extra sanity check.
71
- if (isValidUuid(uuid) === false)
72
- continue;
73
- comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
74
- }
75
- comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
76
- const href = new Url(env['PUBLIC_URL'])
77
- .addPath('admin', 'content', data['collection'], data['item'])
78
- .toString();
79
- const message = `
80
- Hello ${userName(user)},
81
-
82
- ${userName(sender)} has mentioned you in a comment:
83
-
84
- ${comment}
85
-
86
- <a href="${href}">Click here to view.</a>
87
- `;
88
- await this.notificationsService.createOne({
89
- recipient: userID,
90
- sender: sender['id'],
91
- subject: `You were mentioned in ${data['collection']}`,
92
- message,
93
- collection: data['collection'],
94
- item: data['item'],
95
- });
96
- }
97
- catch (err) {
98
- if (isDirectusError(err, ErrorCode.Forbidden)) {
99
- logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
100
- }
101
- else {
102
- throw err;
103
- }
104
- }
105
- }
106
- }
107
- return super.createOne(data, opts);
108
5
  }
109
6
  }
@@ -98,7 +98,7 @@ export class AssetsService {
98
98
  }
99
99
  if (exists) {
100
100
  return {
101
- stream: await storage.location(file.storage).read(assetFilename, range),
101
+ stream: await storage.location(file.storage).read(assetFilename, { range }),
102
102
  file,
103
103
  stat: await storage.location(file.storage).stat(assetFilename),
104
104
  };
@@ -121,7 +121,8 @@ export class AssetsService {
121
121
  reason: 'Server too busy',
122
122
  });
123
123
  }
124
- const readStream = await storage.location(file.storage).read(file.filename_disk, range);
124
+ const version = file.modified_on !== undefined ? String(Math.round(new Date(file.modified_on).getTime() / 1000)) : undefined;
125
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
125
126
  const transformer = getSharpInstance();
126
127
  transformer.timeout({
127
128
  seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
@@ -151,13 +152,13 @@ export class AssetsService {
151
152
  }
152
153
  }
153
154
  return {
154
- stream: await storage.location(file.storage).read(assetFilename, range),
155
+ stream: await storage.location(file.storage).read(assetFilename, { range }),
155
156
  stat: await storage.location(file.storage).stat(assetFilename),
156
157
  file,
157
158
  };
158
159
  }
159
160
  else {
160
- const readStream = await storage.location(file.storage).read(file.filename_disk, range);
161
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range });
161
162
  const stat = await storage.location(file.storage).stat(file.filename_disk);
162
163
  return { stream: readStream, file, stat };
163
164
  }
@@ -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,10 +155,11 @@ 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();
158
160
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
159
- const updatedSchema = await getSchema();
160
161
  for (const nestedActionEvent of nestedActionEvents) {
161
- nestedActionEvent.context.schema = updatedSchema;
162
+ nestedActionEvent.context.schema = this.schema;
162
163
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
163
164
  }
164
165
  }
@@ -196,10 +197,11 @@ export class CollectionsService {
196
197
  if (opts?.autoPurgeSystemCache !== false) {
197
198
  await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
198
199
  }
200
+ // Refresh the schema for subsequent reads
201
+ this.schema = await getSchema();
199
202
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
200
- const updatedSchema = await getSchema();
201
203
  for (const nestedActionEvent of nestedActionEvents) {
202
- nestedActionEvent.context.schema = updatedSchema;
204
+ nestedActionEvent.context.schema = this.schema;
203
205
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
204
206
  }
205
207
  }
@@ -0,0 +1,31 @@
1
+ import type { Comment, Item, PrimaryKey, Query } from '@directus/types';
2
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
+ import { ActivityService } from './activity.js';
4
+ import { ItemsService, type QueryOptions } from './items.js';
5
+ import { NotificationsService } from './notifications.js';
6
+ import { UsersService } from './users.js';
7
+ type serviceOrigin = 'activity' | 'comments';
8
+ export declare class CommentsService extends ItemsService {
9
+ activityService: ActivityService;
10
+ notificationsService: NotificationsService;
11
+ usersService: UsersService;
12
+ serviceOrigin: serviceOrigin;
13
+ constructor(options: AbstractServiceOptions & {
14
+ serviceOrigin: serviceOrigin;
15
+ });
16
+ readOne(key: PrimaryKey, query?: Query, opts?: QueryOptions): Promise<Item>;
17
+ readByQuery(query: Query, opts?: QueryOptions): Promise<Item[]>;
18
+ readMany(keys: PrimaryKey[], query?: Query, opts?: QueryOptions): Promise<Item[]>;
19
+ createOne(data: Partial<Comment>, opts?: MutationOptions): Promise<PrimaryKey>;
20
+ updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
21
+ updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
22
+ updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
23
+ deleteByQuery(query: Query, opts?: MutationOptions): Promise<PrimaryKey[]>;
24
+ deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
25
+ deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey>;
26
+ private processPrimaryKeys;
27
+ migrateLegacyComment(activityPk: PrimaryKey): Promise<PrimaryKey>;
28
+ generateQuery(type: serviceOrigin, originalQuery: Query): Query;
29
+ private sortLegacyResults;
30
+ }
31
+ export {};