@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,378 @@
1
+ import { Action } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
3
+ import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
4
+ import { cloneDeep, mergeWith, uniq } from 'lodash-es';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { useLogger } from '../logger/index.js';
7
+ import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
8
+ import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
9
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
10
+ import { isValidUuid } from '../utils/is-valid-uuid.js';
11
+ import { transaction } from '../utils/transaction.js';
12
+ import { Url } from '../utils/url.js';
13
+ import { userName } from '../utils/user-name.js';
14
+ import { ActivityService } from './activity.js';
15
+ import { ItemsService } from './items.js';
16
+ import { NotificationsService } from './notifications.js';
17
+ import { UsersService } from './users.js';
18
+ const env = useEnv();
19
+ const logger = useLogger();
20
+ // TODO: Remove legacy comments logic
21
+ export class CommentsService extends ItemsService {
22
+ activityService;
23
+ notificationsService;
24
+ usersService;
25
+ serviceOrigin;
26
+ constructor(options) {
27
+ super('directus_comments', options);
28
+ this.activityService = new ActivityService(options);
29
+ this.notificationsService = new NotificationsService({ schema: this.schema });
30
+ this.usersService = new UsersService({ schema: this.schema });
31
+ this.serviceOrigin = options.serviceOrigin ?? 'comments';
32
+ }
33
+ readOne(key, query, opts) {
34
+ const isLegacyComment = isNaN(Number(key));
35
+ let result;
36
+ if (isLegacyComment) {
37
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
38
+ result = this.activityService.readOne(key, activityQuery, opts);
39
+ }
40
+ else {
41
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
42
+ result = super.readOne(key, commentsQuery, opts);
43
+ }
44
+ return result;
45
+ }
46
+ async readByQuery(query, opts) {
47
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query);
48
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query);
49
+ const activityResult = await this.activityService.readByQuery(activityQuery, opts);
50
+ const commentsResult = await super.readByQuery(commentsQuery, opts);
51
+ if (query.aggregate) {
52
+ // Merging the first result only as the app does not utilise group
53
+ return [
54
+ mergeWith({}, activityResult[0], commentsResult[0], (a, b) => {
55
+ const numA = Number(a);
56
+ const numB = Number(b);
57
+ if (!isNaN(numA) && !isNaN(numB)) {
58
+ return numA + numB;
59
+ }
60
+ return;
61
+ }),
62
+ ];
63
+ }
64
+ else if (query.sort) {
65
+ return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
66
+ }
67
+ else {
68
+ return [...activityResult, ...commentsResult];
69
+ }
70
+ }
71
+ async readMany(keys, query, opts) {
72
+ const commentsKeys = [];
73
+ const activityKeys = [];
74
+ for (const key of keys) {
75
+ if (isNaN(Number(key))) {
76
+ commentsKeys.push(key);
77
+ }
78
+ else {
79
+ activityKeys.push(key);
80
+ }
81
+ }
82
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
83
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
84
+ const activityResult = await this.activityService.readMany(activityKeys, activityQuery, opts);
85
+ const commentsResult = await super.readMany(commentsKeys, commentsQuery, opts);
86
+ if (query?.sort) {
87
+ return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
88
+ }
89
+ else {
90
+ return [...activityResult, ...commentsResult];
91
+ }
92
+ }
93
+ async createOne(data, opts) {
94
+ if (!data['comment']) {
95
+ throw new InvalidPayloadError({ reason: `"comment" is required` });
96
+ }
97
+ if (!data['collection']) {
98
+ throw new InvalidPayloadError({ reason: `"collection" is required` });
99
+ }
100
+ if (!data['item']) {
101
+ throw new InvalidPayloadError({ reason: `"item" is required` });
102
+ }
103
+ if (this.accountability) {
104
+ await validateAccess({
105
+ accountability: this.accountability,
106
+ action: 'read',
107
+ collection: data['collection'],
108
+ primaryKeys: [data['item']],
109
+ }, {
110
+ schema: this.schema,
111
+ knex: this.knex,
112
+ });
113
+ }
114
+ 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);
115
+ const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
116
+ const sender = await this.usersService.readOne(this.accountability.user, {
117
+ fields: ['id', 'first_name', 'last_name', 'email'],
118
+ });
119
+ for (const mention of mentions) {
120
+ const userID = mention.substring(1);
121
+ const user = await this.usersService.readOne(userID, {
122
+ fields: ['id', 'first_name', 'last_name', 'email', 'role.id'],
123
+ });
124
+ const accountability = {
125
+ user: userID,
126
+ role: user['role']?.id ?? null,
127
+ admin: false,
128
+ app: false,
129
+ roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
130
+ ip: null,
131
+ };
132
+ const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
133
+ accountability.admin = userGlobalAccess.admin;
134
+ accountability.app = userGlobalAccess.app;
135
+ const usersService = new UsersService({ schema: this.schema, accountability });
136
+ try {
137
+ await validateAccess({
138
+ accountability,
139
+ action: 'read',
140
+ collection: data['collection'],
141
+ primaryKeys: [data['item']],
142
+ }, {
143
+ schema: this.schema,
144
+ knex: this.knex,
145
+ });
146
+ const templateData = await usersService.readByQuery({
147
+ fields: ['id', 'first_name', 'last_name', 'email'],
148
+ filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
149
+ });
150
+ const userPreviews = templateData.reduce((acc, user) => {
151
+ acc[user['id']] = `<em>${userName(user)}</em>`;
152
+ return acc;
153
+ }, {});
154
+ let comment = data['comment'];
155
+ for (const mention of mentions) {
156
+ const uuid = mention.substring(1);
157
+ // We only match on UUIDs in the first place. This is just an extra sanity check.
158
+ if (isValidUuid(uuid) === false)
159
+ continue;
160
+ comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
161
+ }
162
+ comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
163
+ const href = new Url(env['PUBLIC_URL'])
164
+ .addPath('admin', 'content', data['collection'], data['item'])
165
+ .toString();
166
+ const message = `
167
+ Hello ${userName(user)},
168
+
169
+ ${userName(sender)} has mentioned you in a comment:
170
+
171
+ ${comment}
172
+
173
+ <a href="${href}">Click here to view.</a>
174
+ `;
175
+ await this.notificationsService.createOne({
176
+ recipient: userID,
177
+ sender: sender['id'],
178
+ subject: `You were mentioned in ${data['collection']}`,
179
+ message,
180
+ collection: data['collection'],
181
+ item: data['item'],
182
+ });
183
+ }
184
+ catch (err) {
185
+ if (isDirectusError(err, ErrorCode.Forbidden)) {
186
+ logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
187
+ }
188
+ else {
189
+ throw err;
190
+ }
191
+ }
192
+ }
193
+ return super.createOne(data, opts);
194
+ }
195
+ async updateByQuery(query, data, opts) {
196
+ const keys = await this.getKeysByQuery(query);
197
+ const migratedKeys = await this.processPrimaryKeys(keys);
198
+ return super.updateMany(migratedKeys, data, opts);
199
+ }
200
+ async updateMany(keys, data, opts) {
201
+ const migratedKeys = await this.processPrimaryKeys(keys);
202
+ return super.updateMany(migratedKeys, data, opts);
203
+ }
204
+ async updateOne(key, data, opts) {
205
+ const migratedKey = await this.migrateLegacyComment(key);
206
+ return super.updateOne(migratedKey, data, opts);
207
+ }
208
+ async deleteByQuery(query, opts) {
209
+ const keys = await this.getKeysByQuery(query);
210
+ const migratedKeys = await this.processPrimaryKeys(keys);
211
+ return super.deleteMany(migratedKeys, opts);
212
+ }
213
+ async deleteMany(keys, opts) {
214
+ const migratedKeys = await this.processPrimaryKeys(keys);
215
+ return super.deleteMany(migratedKeys, opts);
216
+ }
217
+ async deleteOne(key, opts) {
218
+ const migratedKey = await this.migrateLegacyComment(key);
219
+ return super.deleteOne(migratedKey, opts);
220
+ }
221
+ async processPrimaryKeys(keys) {
222
+ const migratedKeys = [];
223
+ for (const key of keys) {
224
+ if (isNaN(Number(key))) {
225
+ migratedKeys.push(key);
226
+ continue;
227
+ }
228
+ migratedKeys.push(await this.migrateLegacyComment(key));
229
+ }
230
+ return migratedKeys;
231
+ }
232
+ async migrateLegacyComment(activityPk) {
233
+ // Skip migration if not a legacy comment
234
+ if (isNaN(Number(activityPk))) {
235
+ return activityPk;
236
+ }
237
+ return transaction(this.knex, async (trx) => {
238
+ let primaryKey;
239
+ const legacyComment = await trx('directus_activity').select('*').where('id', '=', activityPk).first();
240
+ // Legacy comment
241
+ if (legacyComment['action'] === Action.COMMENT) {
242
+ primaryKey = randomUUID();
243
+ await trx('directus_comments').insert({
244
+ id: primaryKey,
245
+ collection: legacyComment.collection,
246
+ item: legacyComment.item,
247
+ comment: legacyComment.comment,
248
+ user_created: legacyComment.user,
249
+ date_created: legacyComment.timestamp,
250
+ });
251
+ await trx('directus_activity')
252
+ .update({
253
+ action: Action.CREATE,
254
+ collection: 'directus_comments',
255
+ item: primaryKey,
256
+ comment: null,
257
+ })
258
+ .where('id', '=', activityPk);
259
+ }
260
+ // Migrated comment
261
+ else if (legacyComment.collection === 'directus_comment' && legacyComment.action === Action.CREATE) {
262
+ primaryKey = legacyComment.item;
263
+ }
264
+ if (!primaryKey) {
265
+ throw new ForbiddenError();
266
+ }
267
+ return primaryKey;
268
+ });
269
+ }
270
+ generateQuery(type, originalQuery) {
271
+ const query = cloneDeep(originalQuery);
272
+ const defaultActivityCommentFilter = { action: { _eq: Action.COMMENT } };
273
+ const commentsToActivityFieldMap = {
274
+ id: 'id',
275
+ comment: 'comment',
276
+ item: 'item',
277
+ collection: 'collection',
278
+ user_created: 'user',
279
+ date_created: 'timestamp',
280
+ };
281
+ const activityToCommentsFieldMap = {
282
+ id: 'id',
283
+ comment: 'comment',
284
+ item: 'item',
285
+ collection: 'collection',
286
+ user: 'user_created',
287
+ timestamp: 'date_created',
288
+ };
289
+ const targetFieldMap = type === 'activity' ? commentsToActivityFieldMap : activityToCommentsFieldMap;
290
+ for (const key of Object.keys(originalQuery)) {
291
+ switch (key) {
292
+ case 'fields':
293
+ if (!originalQuery.fields)
294
+ break;
295
+ query.fields = [];
296
+ for (const field of originalQuery.fields) {
297
+ if (field === '*') {
298
+ query.fields = ['*'];
299
+ break;
300
+ }
301
+ const parts = field.split('.');
302
+ const firstPart = parts[0];
303
+ if (firstPart && targetFieldMap[firstPart]) {
304
+ query.fields.push(field);
305
+ if (firstPart !== targetFieldMap[firstPart]) {
306
+ (query.alias = query.alias || {})[firstPart] = targetFieldMap[firstPart];
307
+ }
308
+ }
309
+ }
310
+ break;
311
+ case 'filter':
312
+ if (!originalQuery.filter)
313
+ break;
314
+ if (type === 'activity') {
315
+ query.filter = { _and: [defaultActivityCommentFilter, originalQuery.filter] };
316
+ }
317
+ if (type === 'comments' && this.serviceOrigin === 'activity') {
318
+ if ('_and' in originalQuery.filter && Array.isArray(originalQuery.filter['_and'])) {
319
+ query.filter = {
320
+ _and: originalQuery.filter['_and'].filter((andItem) => !('action' in andItem && '_eq' in andItem['action'] && andItem['action']['_eq'] === 'comment')),
321
+ };
322
+ }
323
+ else {
324
+ query.filter = originalQuery.filter;
325
+ }
326
+ }
327
+ break;
328
+ case 'aggregate':
329
+ if (originalQuery.aggregate) {
330
+ query.aggregate = originalQuery.aggregate;
331
+ }
332
+ break;
333
+ case 'sort':
334
+ if (!originalQuery.sort)
335
+ break;
336
+ query.sort = [];
337
+ for (const sort of originalQuery.sort) {
338
+ const isDescending = sort.startsWith('-');
339
+ const field = isDescending ? sort.slice(1) : sort;
340
+ if (field && targetFieldMap[field]) {
341
+ query.sort.push(`${isDescending ? '-' : ''}${targetFieldMap[field]}`);
342
+ }
343
+ }
344
+ break;
345
+ }
346
+ }
347
+ if (type === 'activity' && !query.filter) {
348
+ query.filter = defaultActivityCommentFilter;
349
+ }
350
+ return query;
351
+ }
352
+ sortLegacyResults(results, sort) {
353
+ if (!sort)
354
+ return results;
355
+ let sortKeys = sort;
356
+ // Fix legacy app sort query which uses id
357
+ if (sortKeys.length === 1 && sortKeys[0]?.endsWith('id') && results[0]?.['timestamp']) {
358
+ sortKeys = [`${sortKeys[0].startsWith('-') ? '-' : ''}timestamp`];
359
+ }
360
+ return results.sort((a, b) => {
361
+ for (const key of sortKeys) {
362
+ const isDescending = key.startsWith('-');
363
+ const actualKey = isDescending ? key.substring(1) : key;
364
+ let aValue = a[actualKey];
365
+ let bValue = b[actualKey];
366
+ if (actualKey === 'date_created' || actualKey === 'timestamp') {
367
+ aValue = new Date(aValue);
368
+ bValue = new Date(bValue);
369
+ }
370
+ if (aValue < bValue)
371
+ return isDescending ? 1 : -1;
372
+ if (aValue > bValue)
373
+ return isDescending ? -1 : 1;
374
+ }
375
+ return 0;
376
+ });
377
+ }
378
+ }
@@ -1,4 +1,4 @@
1
- import { Action, FUNCTIONS } from '@directus/constants';
1
+ import { FUNCTIONS } from '@directus/constants';
2
2
  import { useEnv } from '@directus/env';
3
3
  import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
4
4
  import { isSystemCollection } from '@directus/system-data';
@@ -11,6 +11,8 @@ import { clearSystemCache, getCache } from '../../cache.js';
11
11
  import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, } from '../../constants.js';
12
12
  import getDatabase from '../../database/index.js';
13
13
  import { rateLimiter } from '../../middleware/rate-limiter-registration.js';
14
+ import { fetchAccountabilityCollectionAccess } from '../../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js';
15
+ import { fetchAccountabilityPolicyGlobals } from '../../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js';
14
16
  import { fetchAllowedFieldMap } from '../../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
15
17
  import { fetchInconsistentFieldMap } from '../../permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js';
16
18
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
@@ -25,14 +27,15 @@ import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-vers
25
27
  import { reduceSchema } from '../../utils/reduce-schema.js';
26
28
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
27
29
  import { validateQuery } from '../../utils/validate-query.js';
28
- import { ActivityService } from '../activity.js';
29
30
  import { AuthenticationService } from '../authentication.js';
30
31
  import { CollectionsService } from '../collections.js';
32
+ import { CommentsService } from '../comments.js';
31
33
  import { ExtensionsService } from '../extensions.js';
32
34
  import { FieldsService } from '../fields.js';
33
35
  import { FilesService } from '../files.js';
34
36
  import { RelationsService } from '../relations.js';
35
37
  import { RevisionsService } from '../revisions.js';
38
+ import { RolesService } from '../roles.js';
36
39
  import { ServerService } from '../server.js';
37
40
  import { SpecificationService } from '../specifications.js';
38
41
  import { TFAService } from '../tfa.js';
@@ -51,9 +54,6 @@ import { GraphQLVoid } from './types/void.js';
51
54
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
52
55
  import processError from './utils/process-error.js';
53
56
  import { sanitizeGraphqlSchema } from './utils/sanitize-gql-schema.js';
54
- import { fetchAccountabilityCollectionAccess } from '../../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js';
55
- import { fetchAccountabilityPolicyGlobals } from '../../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js';
56
- import { RolesService } from '../roles.js';
57
57
  const env = useEnv();
58
58
  const validationRules = Array.from(specifiedRules);
59
59
  if (env['GRAPHQL_INTROSPECTION'] === false) {
@@ -2313,6 +2313,9 @@ export class GraphQLService {
2313
2313
  max_length: GraphQLInt,
2314
2314
  numeric_precision: GraphQLInt,
2315
2315
  numeric_scale: GraphQLInt,
2316
+ is_generated: GraphQLBoolean,
2317
+ generation_expression: GraphQLString,
2318
+ is_indexed: GraphQLBoolean,
2316
2319
  is_nullable: GraphQLBoolean,
2317
2320
  is_unique: GraphQLBoolean,
2318
2321
  is_primary_key: GraphQLBoolean,
@@ -2771,17 +2774,13 @@ export class GraphQLService {
2771
2774
  comment: new GraphQLNonNull(GraphQLString),
2772
2775
  },
2773
2776
  resolve: async (_, args, __, info) => {
2774
- const service = new ActivityService({
2777
+ const service = new CommentsService({
2775
2778
  accountability: this.accountability,
2776
2779
  schema: this.schema,
2780
+ serviceOrigin: 'activity',
2777
2781
  });
2778
2782
  const primaryKey = await service.createOne({
2779
2783
  ...args,
2780
- action: Action.COMMENT,
2781
- user: this.accountability?.user,
2782
- ip: this.accountability?.ip,
2783
- user_agent: this.accountability?.userAgent,
2784
- origin: this.accountability?.origin,
2785
2784
  });
2786
2785
  if ('directus_activity' in ReadCollectionTypes) {
2787
2786
  const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
@@ -2802,15 +2801,16 @@ export class GraphQLService {
2802
2801
  comment: new GraphQLNonNull(GraphQLString),
2803
2802
  },
2804
2803
  resolve: async (_, args, __, info) => {
2805
- const service = new ActivityService({
2804
+ const commentsService = new CommentsService({
2806
2805
  accountability: this.accountability,
2807
2806
  schema: this.schema,
2807
+ serviceOrigin: 'activity',
2808
2808
  });
2809
- const primaryKey = await service.updateOne(args['id'], { comment: args['comment'] });
2809
+ const primaryKey = await commentsService.updateOne(args['id'], { comment: args['comment'] });
2810
2810
  if ('directus_activity' in ReadCollectionTypes) {
2811
2811
  const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
2812
2812
  const query = this.getQuery(args, selections || [], info.variableValues);
2813
- return await service.readOne(primaryKey, query);
2813
+ return { ...(await commentsService.readOne(primaryKey, query)), id: args['id'] };
2814
2814
  }
2815
2815
  return true;
2816
2816
  },
@@ -2825,11 +2825,12 @@ export class GraphQLService {
2825
2825
  id: new GraphQLNonNull(GraphQLID),
2826
2826
  },
2827
2827
  resolve: async (_, args) => {
2828
- const service = new ActivityService({
2828
+ const commentsService = new CommentsService({
2829
2829
  accountability: this.accountability,
2830
2830
  schema: this.schema,
2831
+ serviceOrigin: 'activity',
2831
2832
  });
2832
- await service.deleteOne(args['id']);
2833
+ await commentsService.deleteOne(args['id']);
2833
2834
  return { id: args['id'] };
2834
2835
  },
2835
2836
  },
@@ -3,6 +3,7 @@ export * from './activity.js';
3
3
  export * from './assets.js';
4
4
  export * from './authentication.js';
5
5
  export * from './collections.js';
6
+ export * from './comments.js';
6
7
  export * from './dashboards.js';
7
8
  export * from './extensions.js';
8
9
  export * from './fields.js';
@@ -3,6 +3,7 @@ export * from './activity.js';
3
3
  export * from './assets.js';
4
4
  export * from './authentication.js';
5
5
  export * from './collections.js';
6
+ export * from './comments.js';
6
7
  export * from './dashboards.js';
7
8
  export * from './extensions.js';
8
9
  export * from './fields.js';
@@ -543,6 +543,7 @@ export class ItemsService {
543
543
  action: 'update',
544
544
  collection: this.collection,
545
545
  primaryKeys: keys,
546
+ fields: Object.keys(payloadAfterHooks),
546
547
  }, {
547
548
  schema: this.schema,
548
549
  knex: this.knex,
@@ -706,7 +707,8 @@ export class ItemsService {
706
707
  .where({ [primaryKeyField]: primaryKey })
707
708
  .first());
708
709
  if (exists) {
709
- return await this.updateOne(primaryKey, payload, opts);
710
+ const { [primaryKeyField]: _, ...data } = payload;
711
+ return await this.updateOne(primaryKey, data, opts);
710
712
  }
711
713
  else {
712
714
  return await this.createOne(payload, opts);
@@ -2,7 +2,8 @@ import type { Accountability, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { SendMailOptions, Transporter } from 'nodemailer';
4
4
  import type { AbstractServiceOptions } from '../../types/index.js';
5
- export type EmailOptions = SendMailOptions & {
5
+ export type EmailOptions = Omit<SendMailOptions, 'from'> & {
6
+ from?: string;
6
7
  template?: {
7
8
  name: string;
8
9
  data: Record<string, any>;
@@ -42,7 +42,10 @@ export class MailService {
42
42
  const { template, ...emailOptions } = payload;
43
43
  let { html } = options;
44
44
  const defaultTemplateData = await this.getDefaultTemplateData();
45
- const from = `${defaultTemplateData.projectName} <${options.from || env['EMAIL_FROM']}>`;
45
+ const from = {
46
+ name: defaultTemplateData.projectName,
47
+ address: options.from || env['EMAIL_FROM'],
48
+ };
46
49
  if (template) {
47
50
  let templateData = template.data;
48
51
  templateData = {
@@ -3,7 +3,7 @@ import { parseJSON, toArray } from '@directus/utils';
3
3
  import { format, isValid, parseISO } from 'date-fns';
4
4
  import { unflatten } from 'flat';
5
5
  import Joi from 'joi';
6
- import { clone, cloneDeep, isNil, isObject, isPlainObject, omit, pick } from 'lodash-es';
6
+ import { clone, cloneDeep, isNil, isObject, isPlainObject, pick } from 'lodash-es';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { parse as wktToGeoJSON } from 'wellknown';
9
9
  import { getHelpers } from '../database/helpers/index.js';
@@ -347,22 +347,22 @@ export class PayloadService {
347
347
  knex: this.knex,
348
348
  schema: this.schema,
349
349
  });
350
- const relatedPrimary = this.schema.collections[relatedCollection].primary;
350
+ const relatedPrimaryKeyField = this.schema.collections[relatedCollection].primary;
351
351
  const relatedRecord = payload[relation.field];
352
352
  if (['string', 'number'].includes(typeof relatedRecord))
353
353
  continue;
354
- const hasPrimaryKey = relatedPrimary in relatedRecord;
355
- let relatedPrimaryKey = relatedRecord[relatedPrimary];
354
+ const hasPrimaryKey = relatedPrimaryKeyField in relatedRecord;
355
+ let relatedPrimaryKey = relatedRecord[relatedPrimaryKeyField];
356
356
  const exists = hasPrimaryKey &&
357
357
  !!(await this.knex
358
- .select(relatedPrimary)
358
+ .select(relatedPrimaryKeyField)
359
359
  .from(relatedCollection)
360
- .where({ [relatedPrimary]: relatedPrimaryKey })
360
+ .where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
361
361
  .first());
362
362
  if (exists) {
363
- const fieldsToUpdate = omit(relatedRecord, relatedPrimary);
364
- if (Object.keys(fieldsToUpdate).length > 0) {
365
- await service.updateOne(relatedPrimaryKey, relatedRecord, {
363
+ const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
364
+ if (Object.keys(record).length > 0) {
365
+ await service.updateOne(relatedPrimaryKey, record, {
366
366
  onRevisionCreate: (pk) => revisions.push(pk),
367
367
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
368
368
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
@@ -425,9 +425,9 @@ export class PayloadService {
425
425
  .where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
426
426
  .first());
427
427
  if (exists) {
428
- const fieldsToUpdate = omit(relatedRecord, relatedPrimaryKeyField);
429
- if (Object.keys(fieldsToUpdate).length > 0) {
430
- await service.updateOne(relatedPrimaryKey, relatedRecord, {
428
+ const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
429
+ if (Object.keys(record).length > 0) {
430
+ await service.updateOne(relatedPrimaryKey, record, {
431
431
  onRevisionCreate: (pk) => revisions.push(pk),
432
432
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
433
433
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
@@ -610,8 +610,9 @@ export class PayloadService {
610
610
  if (alterations.update) {
611
611
  const primaryKeyField = this.schema.collections[relation.collection].primary;
612
612
  for (const item of alterations.update) {
613
- await service.updateOne(item[primaryKeyField], {
614
- ...item,
613
+ const { [primaryKeyField]: key, ...record } = item;
614
+ await service.updateOne(key, {
615
+ ...record,
615
616
  [relation.field]: parent || payload[currentPrimaryKeyField],
616
617
  }, {
617
618
  onRevisionCreate: (pk) => revisions.push(pk),
@@ -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'>>;