@directus/api 22.2.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/drivers/ldap.js +14 -3
  3. package/dist/auth/drivers/oauth2.js +13 -2
  4. package/dist/auth/drivers/openid.js +13 -2
  5. package/dist/cache.js +4 -4
  6. package/dist/cli/commands/init/questions.d.ts +5 -5
  7. package/dist/cli/commands/schema/apply.d.ts +1 -0
  8. package/dist/cli/commands/schema/apply.js +20 -1
  9. package/dist/cli/index.js +1 -0
  10. package/dist/cli/utils/create-env/env-stub.liquid +1 -4
  11. package/dist/controllers/activity.js +30 -27
  12. package/dist/controllers/assets.js +1 -1
  13. package/dist/controllers/comments.d.ts +2 -0
  14. package/dist/controllers/comments.js +153 -0
  15. package/dist/controllers/versions.js +10 -5
  16. package/dist/database/index.js +3 -0
  17. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  18. package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
  19. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  20. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  21. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  22. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  23. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  24. package/dist/database/run-ast/lib/get-db-query.js +2 -2
  25. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  26. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  27. package/dist/database/run-ast/run-ast.js +8 -1
  28. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  29. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  30. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
  31. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
  32. package/dist/logger/index.d.ts +2 -3
  33. package/dist/logger/logs-stream.d.ts +0 -1
  34. package/dist/mailer.js +0 -6
  35. package/dist/middleware/authenticate.d.ts +1 -3
  36. package/dist/middleware/error-handler.d.ts +0 -1
  37. package/dist/middleware/validate-batch.d.ts +1 -4
  38. package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
  39. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
  40. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  41. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  42. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  43. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  44. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
  46. package/dist/permissions/utils/process-permissions.d.ts +11 -1
  47. package/dist/permissions/utils/process-permissions.js +6 -4
  48. package/dist/request/agent-with-ip-validation.d.ts +0 -1
  49. package/dist/server.d.ts +0 -3
  50. package/dist/services/activity.d.ts +1 -7
  51. package/dist/services/activity.js +0 -103
  52. package/dist/services/assets.d.ts +0 -1
  53. package/dist/services/assets.js +5 -4
  54. package/dist/services/collections.js +6 -4
  55. package/dist/services/comments.d.ts +31 -0
  56. package/dist/services/comments.js +374 -0
  57. package/dist/services/fields.js +0 -6
  58. package/dist/services/files/utils/get-metadata.d.ts +0 -1
  59. package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
  60. package/dist/services/files.d.ts +0 -1
  61. package/dist/services/graphql/index.js +17 -16
  62. package/dist/services/import-export.d.ts +0 -1
  63. package/dist/services/index.d.ts +1 -0
  64. package/dist/services/index.js +1 -0
  65. package/dist/services/items.js +3 -1
  66. package/dist/services/mail/index.d.ts +2 -1
  67. package/dist/services/mail/index.js +4 -1
  68. package/dist/services/payload.js +15 -14
  69. package/dist/services/tus/data-store.d.ts +0 -1
  70. package/dist/services/users.js +3 -2
  71. package/dist/services/versions.js +59 -44
  72. package/dist/types/graphql.d.ts +0 -1
  73. package/dist/utils/apply-diff.js +5 -6
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/compress.d.ts +0 -1
  76. package/dist/utils/delete-from-require-cache.js +1 -1
  77. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
  78. package/dist/utils/generate-hash.js +2 -2
  79. package/dist/utils/get-address.d.ts +0 -3
  80. package/dist/utils/get-cache-headers.d.ts +0 -1
  81. package/dist/utils/get-cache-key.d.ts +0 -1
  82. package/dist/utils/get-column.d.ts +1 -1
  83. package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
  84. package/dist/utils/get-ip-from-req.d.ts +0 -1
  85. package/dist/utils/get-service.js +3 -1
  86. package/dist/utils/get-snapshot.js +1 -1
  87. package/dist/utils/sanitize-query.js +1 -1
  88. package/dist/utils/sanitize-schema.d.ts +1 -1
  89. package/dist/utils/sanitize-schema.js +2 -0
  90. package/dist/utils/should-skip-cache.d.ts +0 -1
  91. package/dist/websocket/authenticate.js +1 -1
  92. package/dist/websocket/controllers/base.d.ts +1 -10
  93. package/dist/websocket/controllers/base.js +15 -21
  94. package/dist/websocket/controllers/graphql.d.ts +0 -3
  95. package/dist/websocket/controllers/index.d.ts +0 -3
  96. package/dist/websocket/controllers/logs.d.ts +4 -5
  97. package/dist/websocket/controllers/logs.js +7 -3
  98. package/dist/websocket/controllers/rest.d.ts +0 -3
  99. package/dist/websocket/controllers/rest.js +1 -1
  100. package/dist/websocket/types.d.ts +0 -6
  101. package/package.json +70 -71
@@ -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 {};
@@ -0,0 +1,374 @@
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 { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
9
+ import { isValidUuid } from '../utils/is-valid-uuid.js';
10
+ import { transaction } from '../utils/transaction.js';
11
+ import { Url } from '../utils/url.js';
12
+ import { userName } from '../utils/user-name.js';
13
+ import { ActivityService } from './activity.js';
14
+ import { ItemsService } from './items.js';
15
+ import { NotificationsService } from './notifications.js';
16
+ import { UsersService } from './users.js';
17
+ const env = useEnv();
18
+ const logger = useLogger();
19
+ // TODO: Remove legacy comments logic
20
+ export class CommentsService extends ItemsService {
21
+ activityService;
22
+ notificationsService;
23
+ usersService;
24
+ serviceOrigin;
25
+ constructor(options) {
26
+ super('directus_comments', options);
27
+ this.activityService = new ActivityService(options);
28
+ this.notificationsService = new NotificationsService({ schema: this.schema });
29
+ this.usersService = new UsersService({ schema: this.schema });
30
+ this.serviceOrigin = options.serviceOrigin ?? 'comments';
31
+ }
32
+ readOne(key, query, opts) {
33
+ const isLegacyComment = isNaN(Number(key));
34
+ let result;
35
+ if (isLegacyComment) {
36
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
37
+ result = this.activityService.readOne(key, activityQuery, opts);
38
+ }
39
+ else {
40
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
41
+ result = super.readOne(key, commentsQuery, opts);
42
+ }
43
+ return result;
44
+ }
45
+ async readByQuery(query, opts) {
46
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query);
47
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query);
48
+ const activityResult = await this.activityService.readByQuery(activityQuery, opts);
49
+ const commentsResult = await super.readByQuery(commentsQuery, opts);
50
+ if (query.aggregate) {
51
+ // Merging the first result only as the app does not utilise group
52
+ return [
53
+ mergeWith({}, activityResult[0], commentsResult[0], (a, b) => {
54
+ const numA = Number(a);
55
+ const numB = Number(b);
56
+ if (!isNaN(numA) && !isNaN(numB)) {
57
+ return numA + numB;
58
+ }
59
+ return;
60
+ }),
61
+ ];
62
+ }
63
+ else if (query.sort) {
64
+ return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
65
+ }
66
+ else {
67
+ return [...activityResult, ...commentsResult];
68
+ }
69
+ }
70
+ async readMany(keys, query, opts) {
71
+ const commentsKeys = [];
72
+ const activityKeys = [];
73
+ for (const key of keys) {
74
+ if (isNaN(Number(key))) {
75
+ commentsKeys.push(key);
76
+ }
77
+ else {
78
+ activityKeys.push(key);
79
+ }
80
+ }
81
+ const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
82
+ const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
83
+ const activityResult = await this.activityService.readMany(activityKeys, activityQuery, opts);
84
+ const commentsResult = await super.readMany(commentsKeys, commentsQuery, opts);
85
+ if (query?.sort) {
86
+ return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
87
+ }
88
+ else {
89
+ return [...activityResult, ...commentsResult];
90
+ }
91
+ }
92
+ async createOne(data, opts) {
93
+ if (!data['comment']) {
94
+ throw new InvalidPayloadError({ reason: `"comment" is required` });
95
+ }
96
+ if (!data['collection']) {
97
+ throw new InvalidPayloadError({ reason: `"collection" is required` });
98
+ }
99
+ if (!data['item']) {
100
+ throw new InvalidPayloadError({ reason: `"item" is required` });
101
+ }
102
+ if (this.accountability) {
103
+ await validateAccess({
104
+ accountability: this.accountability,
105
+ action: 'read',
106
+ collection: data['collection'],
107
+ primaryKeys: [data['item']],
108
+ }, {
109
+ schema: this.schema,
110
+ knex: this.knex,
111
+ });
112
+ }
113
+ 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);
114
+ const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
115
+ const sender = await this.usersService.readOne(this.accountability.user, {
116
+ fields: ['id', 'first_name', 'last_name', 'email'],
117
+ });
118
+ for (const mention of mentions) {
119
+ const userID = mention.substring(1);
120
+ const user = await this.usersService.readOne(userID, {
121
+ fields: ['id', 'first_name', 'last_name', 'email', 'role.id', 'role.admin_access', 'role.app_access'],
122
+ });
123
+ const accountability = {
124
+ user: userID,
125
+ 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),
129
+ ip: null,
130
+ };
131
+ const usersService = new UsersService({ schema: this.schema, accountability });
132
+ try {
133
+ await validateAccess({
134
+ accountability,
135
+ action: 'read',
136
+ collection: data['collection'],
137
+ primaryKeys: [data['item']],
138
+ }, {
139
+ schema: this.schema,
140
+ knex: this.knex,
141
+ });
142
+ const templateData = await usersService.readByQuery({
143
+ fields: ['id', 'first_name', 'last_name', 'email'],
144
+ filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
145
+ });
146
+ const userPreviews = templateData.reduce((acc, user) => {
147
+ acc[user['id']] = `<em>${userName(user)}</em>`;
148
+ return acc;
149
+ }, {});
150
+ let comment = data['comment'];
151
+ for (const mention of mentions) {
152
+ const uuid = mention.substring(1);
153
+ // We only match on UUIDs in the first place. This is just an extra sanity check.
154
+ if (isValidUuid(uuid) === false)
155
+ continue;
156
+ comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
157
+ }
158
+ comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
159
+ const href = new Url(env['PUBLIC_URL'])
160
+ .addPath('admin', 'content', data['collection'], data['item'])
161
+ .toString();
162
+ const message = `
163
+ Hello ${userName(user)},
164
+
165
+ ${userName(sender)} has mentioned you in a comment:
166
+
167
+ ${comment}
168
+
169
+ <a href="${href}">Click here to view.</a>
170
+ `;
171
+ await this.notificationsService.createOne({
172
+ recipient: userID,
173
+ sender: sender['id'],
174
+ subject: `You were mentioned in ${data['collection']}`,
175
+ message,
176
+ collection: data['collection'],
177
+ item: data['item'],
178
+ });
179
+ }
180
+ catch (err) {
181
+ if (isDirectusError(err, ErrorCode.Forbidden)) {
182
+ logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
183
+ }
184
+ else {
185
+ throw err;
186
+ }
187
+ }
188
+ }
189
+ return super.createOne(data, opts);
190
+ }
191
+ async updateByQuery(query, data, opts) {
192
+ const keys = await this.getKeysByQuery(query);
193
+ const migratedKeys = await this.processPrimaryKeys(keys);
194
+ return super.updateMany(migratedKeys, data, opts);
195
+ }
196
+ async updateMany(keys, data, opts) {
197
+ const migratedKeys = await this.processPrimaryKeys(keys);
198
+ return super.updateMany(migratedKeys, data, opts);
199
+ }
200
+ async updateOne(key, data, opts) {
201
+ const migratedKey = await this.migrateLegacyComment(key);
202
+ return super.updateOne(migratedKey, data, opts);
203
+ }
204
+ async deleteByQuery(query, opts) {
205
+ const keys = await this.getKeysByQuery(query);
206
+ const migratedKeys = await this.processPrimaryKeys(keys);
207
+ return super.deleteMany(migratedKeys, opts);
208
+ }
209
+ async deleteMany(keys, opts) {
210
+ const migratedKeys = await this.processPrimaryKeys(keys);
211
+ return super.deleteMany(migratedKeys, opts);
212
+ }
213
+ async deleteOne(key, opts) {
214
+ const migratedKey = await this.migrateLegacyComment(key);
215
+ return super.deleteOne(migratedKey, opts);
216
+ }
217
+ async processPrimaryKeys(keys) {
218
+ const migratedKeys = [];
219
+ for (const key of keys) {
220
+ if (isNaN(Number(key))) {
221
+ migratedKeys.push(key);
222
+ continue;
223
+ }
224
+ migratedKeys.push(await this.migrateLegacyComment(key));
225
+ }
226
+ return migratedKeys;
227
+ }
228
+ async migrateLegacyComment(activityPk) {
229
+ // Skip migration if not a legacy comment
230
+ if (isNaN(Number(activityPk))) {
231
+ return activityPk;
232
+ }
233
+ return transaction(this.knex, async (trx) => {
234
+ let primaryKey;
235
+ const legacyComment = await trx('directus_activity').select('*').where('id', '=', activityPk).first();
236
+ // Legacy comment
237
+ if (legacyComment['action'] === Action.COMMENT) {
238
+ primaryKey = randomUUID();
239
+ await trx('directus_comments').insert({
240
+ id: primaryKey,
241
+ collection: legacyComment.collection,
242
+ item: legacyComment.item,
243
+ comment: legacyComment.comment,
244
+ user_created: legacyComment.user,
245
+ date_created: legacyComment.timestamp,
246
+ });
247
+ await trx('directus_activity')
248
+ .update({
249
+ action: Action.CREATE,
250
+ collection: 'directus_comments',
251
+ item: primaryKey,
252
+ comment: null,
253
+ })
254
+ .where('id', '=', activityPk);
255
+ }
256
+ // Migrated comment
257
+ else if (legacyComment.collection === 'directus_comment' && legacyComment.action === Action.CREATE) {
258
+ primaryKey = legacyComment.item;
259
+ }
260
+ if (!primaryKey) {
261
+ throw new ForbiddenError();
262
+ }
263
+ return primaryKey;
264
+ });
265
+ }
266
+ generateQuery(type, originalQuery) {
267
+ const query = cloneDeep(originalQuery);
268
+ const defaultActivityCommentFilter = { action: { _eq: Action.COMMENT } };
269
+ const commentsToActivityFieldMap = {
270
+ id: 'id',
271
+ comment: 'comment',
272
+ item: 'item',
273
+ collection: 'collection',
274
+ user_created: 'user',
275
+ date_created: 'timestamp',
276
+ };
277
+ const activityToCommentsFieldMap = {
278
+ id: 'id',
279
+ comment: 'comment',
280
+ item: 'item',
281
+ collection: 'collection',
282
+ user: 'user_created',
283
+ timestamp: 'date_created',
284
+ };
285
+ const targetFieldMap = type === 'activity' ? commentsToActivityFieldMap : activityToCommentsFieldMap;
286
+ for (const key of Object.keys(originalQuery)) {
287
+ switch (key) {
288
+ case 'fields':
289
+ if (!originalQuery.fields)
290
+ break;
291
+ query.fields = [];
292
+ for (const field of originalQuery.fields) {
293
+ if (field === '*') {
294
+ query.fields = ['*'];
295
+ break;
296
+ }
297
+ const parts = field.split('.');
298
+ const firstPart = parts[0];
299
+ if (firstPart && targetFieldMap[firstPart]) {
300
+ query.fields.push(field);
301
+ if (firstPart !== targetFieldMap[firstPart]) {
302
+ (query.alias = query.alias || {})[firstPart] = targetFieldMap[firstPart];
303
+ }
304
+ }
305
+ }
306
+ break;
307
+ case 'filter':
308
+ if (!originalQuery.filter)
309
+ break;
310
+ if (type === 'activity') {
311
+ query.filter = { _and: [defaultActivityCommentFilter, originalQuery.filter] };
312
+ }
313
+ if (type === 'comments' && this.serviceOrigin === 'activity') {
314
+ if ('_and' in originalQuery.filter && Array.isArray(originalQuery.filter['_and'])) {
315
+ query.filter = {
316
+ _and: originalQuery.filter['_and'].filter((andItem) => !('action' in andItem && '_eq' in andItem['action'] && andItem['action']['_eq'] === 'comment')),
317
+ };
318
+ }
319
+ else {
320
+ query.filter = originalQuery.filter;
321
+ }
322
+ }
323
+ break;
324
+ case 'aggregate':
325
+ if (originalQuery.aggregate) {
326
+ query.aggregate = originalQuery.aggregate;
327
+ }
328
+ break;
329
+ case 'sort':
330
+ if (!originalQuery.sort)
331
+ break;
332
+ query.sort = [];
333
+ for (const sort of originalQuery.sort) {
334
+ const isDescending = sort.startsWith('-');
335
+ const field = isDescending ? sort.slice(1) : sort;
336
+ if (field && targetFieldMap[field]) {
337
+ query.sort.push(`${isDescending ? '-' : ''}${targetFieldMap[field]}`);
338
+ }
339
+ }
340
+ break;
341
+ }
342
+ }
343
+ if (type === 'activity' && !query.filter) {
344
+ query.filter = defaultActivityCommentFilter;
345
+ }
346
+ return query;
347
+ }
348
+ sortLegacyResults(results, sort) {
349
+ if (!sort)
350
+ return results;
351
+ let sortKeys = sort;
352
+ // Fix legacy app sort query which uses id
353
+ if (sortKeys.length === 1 && sortKeys[0]?.endsWith('id') && results[0]?.['timestamp']) {
354
+ sortKeys = [`${sortKeys[0].startsWith('-') ? '-' : ''}timestamp`];
355
+ }
356
+ return results.sort((a, b) => {
357
+ for (const key of sortKeys) {
358
+ const isDescending = key.startsWith('-');
359
+ const actualKey = isDescending ? key.substring(1) : key;
360
+ let aValue = a[actualKey];
361
+ let bValue = b[actualKey];
362
+ if (actualKey === 'date_created' || actualKey === 'timestamp') {
363
+ aValue = new Date(aValue);
364
+ bValue = new Date(bValue);
365
+ }
366
+ if (aValue < bValue)
367
+ return isDescending ? 1 : -1;
368
+ if (aValue > bValue)
369
+ return isDescending ? -1 : 1;
370
+ }
371
+ return 0;
372
+ });
373
+ }
374
+ }
@@ -388,12 +388,6 @@ export class FieldsService {
388
388
  if (hookAdjustedField.schema?.is_nullable === true) {
389
389
  throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
390
390
  }
391
- if (hookAdjustedField.schema?.is_unique === false) {
392
- throw new InvalidPayloadError({ reason: 'Primary key must be unique' });
393
- }
394
- if (hookAdjustedField.schema?.is_indexed === true) {
395
- throw new InvalidPayloadError({ reason: 'Primary key cannot be indexed' });
396
- }
397
391
  }
398
392
  // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
399
393
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { File } from '@directus/types';
3
2
  import type { Readable } from 'node:stream';
4
3
  export type Metadata = Partial<Pick<File, 'height' | 'width' | 'description' | 'title' | 'tags' | 'metadata'>>;
@@ -1,3 +1,2 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  export declare function parseIptc(buffer: Buffer): Record<string, unknown>;
3
2
  export declare function parseXmp(buffer: Buffer): Record<string, unknown>;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { BusboyFileStream, File, PrimaryKey, Query } from '@directus/types';
3
2
  import type { Readable } from 'node:stream';
4
3
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
@@ -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
  },
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Accountability, File, Query, SchemaOverview } from '@directus/types';
3
2
  import type { Knex } from 'knex';
4
3
  import type { Readable } from 'node:stream';
@@ -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 = {