@directus/api 23.1.3 → 23.2.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 (36) hide show
  1. package/dist/app.js +7 -4
  2. package/dist/auth/drivers/openid.js +1 -1
  3. package/dist/controllers/activity.js +2 -88
  4. package/dist/controllers/comments.js +0 -7
  5. package/dist/controllers/tus.d.ts +0 -1
  6. package/dist/controllers/tus.js +0 -16
  7. package/dist/controllers/versions.js +1 -8
  8. package/dist/database/migrations/20240909A-separate-comments.js +1 -6
  9. package/dist/database/migrations/20240924A-migrate-legacy-comments.d.ts +3 -0
  10. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +59 -0
  11. package/dist/database/migrations/20240924B-populate-versioning-deltas.d.ts +3 -0
  12. package/dist/database/migrations/20240924B-populate-versioning-deltas.js +32 -0
  13. package/dist/database/run-ast/utils/apply-parent-filters.js +4 -0
  14. package/dist/schedules/retention.d.ts +14 -0
  15. package/dist/schedules/retention.js +96 -0
  16. package/dist/{telemetry/lib/init-telemetry.d.ts → schedules/telemetry.d.ts} +2 -2
  17. package/dist/{telemetry/lib/init-telemetry.js → schedules/telemetry.js} +6 -6
  18. package/dist/schedules/tus.d.ts +6 -0
  19. package/dist/schedules/tus.js +23 -0
  20. package/dist/services/assets.js +4 -3
  21. package/dist/services/comments.d.ts +4 -22
  22. package/dist/services/comments.js +16 -252
  23. package/dist/services/graphql/index.d.ts +1 -2
  24. package/dist/services/graphql/index.js +1 -75
  25. package/dist/services/specifications.js +12 -1
  26. package/dist/services/users.js +1 -1
  27. package/dist/services/versions.d.ts +0 -1
  28. package/dist/services/versions.js +9 -29
  29. package/dist/telemetry/index.d.ts +0 -1
  30. package/dist/telemetry/index.js +0 -1
  31. package/dist/utils/apply-diff.js +15 -3
  32. package/dist/utils/get-service.js +1 -1
  33. package/dist/utils/get-snapshot-diff.js +17 -1
  34. package/dist/websocket/controllers/base.js +2 -1
  35. package/dist/websocket/controllers/graphql.js +2 -1
  36. package/package.json +18 -18
@@ -1,96 +1,29 @@
1
- import { Action } from '@directus/constants';
2
1
  import { useEnv } from '@directus/env';
3
2
  import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
4
- import { cloneDeep, mergeWith, uniq } from 'lodash-es';
5
- import { randomUUID } from 'node:crypto';
3
+ import { uniq } from 'lodash-es';
6
4
  import { useLogger } from '../logger/index.js';
7
5
  import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
8
6
  import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
9
7
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
10
8
  import { isValidUuid } from '../utils/is-valid-uuid.js';
11
- import { transaction } from '../utils/transaction.js';
12
9
  import { Url } from '../utils/url.js';
13
10
  import { userName } from '../utils/user-name.js';
14
- import { ActivityService } from './activity.js';
15
11
  import { ItemsService } from './items.js';
16
12
  import { NotificationsService } from './notifications.js';
17
13
  import { UsersService } from './users.js';
18
14
  const env = useEnv();
19
15
  const logger = useLogger();
20
- // TODO: Remove legacy comments logic
21
16
  export class CommentsService extends ItemsService {
22
- activityService;
23
17
  notificationsService;
24
18
  usersService;
25
- serviceOrigin;
26
19
  constructor(options) {
27
20
  super('directus_comments', options);
28
- this.activityService = new ActivityService(options);
29
21
  this.notificationsService = new NotificationsService({ schema: this.schema });
30
22
  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
23
  }
93
24
  async createOne(data, opts) {
25
+ if (!this.accountability?.user)
26
+ throw new ForbiddenError();
94
27
  if (!data['comment']) {
95
28
  throw new InvalidPayloadError({ reason: `"comment" is required` });
96
29
  }
@@ -111,8 +44,12 @@ export class CommentsService extends ItemsService {
111
44
  knex: this.knex,
112
45
  });
113
46
  }
47
+ const result = await super.createOne(data, opts);
114
48
  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
49
  const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
50
+ if (mentions.length === 0) {
51
+ return result;
52
+ }
116
53
  const sender = await this.usersService.readOne(this.accountability.user, {
117
54
  fields: ['id', 'first_name', 'last_name', 'email'],
118
55
  });
@@ -190,189 +127,16 @@ ${comment}
190
127
  }
191
128
  }
192
129
  }
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
- });
130
+ return result;
269
131
  }
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;
132
+ updateOne(key, data, opts) {
133
+ if (!this.accountability?.user)
134
+ throw new ForbiddenError();
135
+ return super.updateOne(key, data, opts);
351
136
  }
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
- });
137
+ deleteOne(key, opts) {
138
+ if (!this.accountability?.user)
139
+ throw new ForbiddenError();
140
+ return super.deleteOne(key, opts);
377
141
  }
378
142
  }
@@ -67,11 +67,10 @@ export declare class GraphQLService {
67
67
  * Effectively merges the selections with the fragments used in those selections
68
68
  */
69
69
  replaceFragmentsInSelections(selections: readonly SelectionNode[] | undefined, fragments: Record<string, FragmentDefinitionNode>): readonly SelectionNode[] | null;
70
- injectSystemResolvers(schemaComposer: SchemaComposer<GraphQLParams['contextValue']>, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes, }: {
70
+ injectSystemResolvers(schemaComposer: SchemaComposer<GraphQLParams['contextValue']>, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, }: {
71
71
  CreateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
72
72
  ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
73
73
  UpdateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
74
- DeleteCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
75
74
  }, schema: {
76
75
  create: SchemaOverview;
77
76
  read: SchemaOverview;
@@ -29,7 +29,6 @@ import { sanitizeQuery } from '../../utils/sanitize-query.js';
29
29
  import { validateQuery } from '../../utils/validate-query.js';
30
30
  import { AuthenticationService } from '../authentication.js';
31
31
  import { CollectionsService } from '../collections.js';
32
- import { CommentsService } from '../comments.js';
33
32
  import { ExtensionsService } from '../extensions.js';
34
33
  import { FieldsService } from '../fields.js';
35
34
  import { FilesService } from '../files.js';
@@ -196,7 +195,6 @@ export class GraphQLService {
196
195
  CreateCollectionTypes,
197
196
  ReadCollectionTypes,
198
197
  UpdateCollectionTypes,
199
- DeleteCollectionTypes,
200
198
  }, schema);
201
199
  }
202
200
  const readableCollections = Object.values(schema.read.collections)
@@ -1650,7 +1648,7 @@ export class GraphQLService {
1650
1648
  })).filter((s) => s);
1651
1649
  return result;
1652
1650
  }
1653
- injectSystemResolvers(schemaComposer, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes, }, schema) {
1651
+ injectSystemResolvers(schemaComposer, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, }, schema) {
1654
1652
  const AuthTokens = schemaComposer.createObjectTC({
1655
1653
  name: 'auth_tokens',
1656
1654
  fields: {
@@ -2764,78 +2762,6 @@ export class GraphQLService {
2764
2762
  },
2765
2763
  });
2766
2764
  }
2767
- if ('directus_activity' in schema.create.collections) {
2768
- schemaComposer.Mutation.addFields({
2769
- create_comment: {
2770
- type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
2771
- args: {
2772
- collection: new GraphQLNonNull(GraphQLString),
2773
- item: new GraphQLNonNull(GraphQLID),
2774
- comment: new GraphQLNonNull(GraphQLString),
2775
- },
2776
- resolve: async (_, args, __, info) => {
2777
- const service = new CommentsService({
2778
- accountability: this.accountability,
2779
- schema: this.schema,
2780
- serviceOrigin: 'activity',
2781
- });
2782
- const primaryKey = await service.createOne({
2783
- ...args,
2784
- });
2785
- if ('directus_activity' in ReadCollectionTypes) {
2786
- const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
2787
- const query = this.getQuery(args, selections || [], info.variableValues);
2788
- return await service.readOne(primaryKey, query);
2789
- }
2790
- return true;
2791
- },
2792
- },
2793
- });
2794
- }
2795
- if ('directus_activity' in schema.update.collections) {
2796
- schemaComposer.Mutation.addFields({
2797
- update_comment: {
2798
- type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
2799
- args: {
2800
- id: new GraphQLNonNull(GraphQLID),
2801
- comment: new GraphQLNonNull(GraphQLString),
2802
- },
2803
- resolve: async (_, args, __, info) => {
2804
- const commentsService = new CommentsService({
2805
- accountability: this.accountability,
2806
- schema: this.schema,
2807
- serviceOrigin: 'activity',
2808
- });
2809
- const primaryKey = await commentsService.updateOne(args['id'], { comment: args['comment'] });
2810
- if ('directus_activity' in ReadCollectionTypes) {
2811
- const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
2812
- const query = this.getQuery(args, selections || [], info.variableValues);
2813
- return { ...(await commentsService.readOne(primaryKey, query)), id: args['id'] };
2814
- }
2815
- return true;
2816
- },
2817
- },
2818
- });
2819
- }
2820
- if ('directus_activity' in schema.delete.collections) {
2821
- schemaComposer.Mutation.addFields({
2822
- delete_comment: {
2823
- type: DeleteCollectionTypes['one'],
2824
- args: {
2825
- id: new GraphQLNonNull(GraphQLID),
2826
- },
2827
- resolve: async (_, args) => {
2828
- const commentsService = new CommentsService({
2829
- accountability: this.accountability,
2830
- schema: this.schema,
2831
- serviceOrigin: 'activity',
2832
- });
2833
- await commentsService.deleteOne(args['id']);
2834
- return { id: args['id'] };
2835
- },
2836
- },
2837
- });
2838
- }
2839
2765
  if ('directus_files' in schema.create.collections) {
2840
2766
  schemaComposer.Mutation.addFields({
2841
2767
  import_file: {
@@ -299,8 +299,19 @@ class OASSpecsService {
299
299
  properties: {},
300
300
  'x-collection': collection.collection,
301
301
  };
302
+ // Track required fields
303
+ const requiredFields = [];
302
304
  for (const field of fieldsInCollection) {
303
- schemaComponent.properties[field.field] = this.generateField(schema, collection.collection, field, tags);
305
+ const fieldSchema = this.generateField(schema, collection.collection, field, tags);
306
+ schemaComponent.properties[field.field] = fieldSchema;
307
+ // Check if field is required
308
+ if (field.nullable === false && field.defaultValue === null && field.generated === false) {
309
+ requiredFields.push(field.field);
310
+ }
311
+ }
312
+ // Only add required if there are actually required fields
313
+ if (requiredFields.length > 0) {
314
+ schemaComponent.required = requiredFields;
304
315
  }
305
316
  components.schemas[tag.name] = schemaComponent;
306
317
  }
@@ -131,7 +131,7 @@ export class UsersService extends ItemsService {
131
131
  */
132
132
  async createOne(data, opts = {}) {
133
133
  try {
134
- if ('email' in data) {
134
+ if ('email' in data && data['email'] !== undefined) {
135
135
  this.validateEmail(data['email']);
136
136
  await this.checkUniqueEmails([data['email']]);
137
137
  }
@@ -9,7 +9,6 @@ export declare class VersionsService extends ItemsService {
9
9
  outdated: boolean;
10
10
  mainHash: string;
11
11
  }>;
12
- getVersionSavesById(id: PrimaryKey): Promise<Partial<Item>[]>;
13
12
  getVersionSaves(key: string, collection: string, item: string | undefined): Promise<Partial<Item>[] | null>;
14
13
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
15
14
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -82,17 +82,6 @@ export class VersionsService extends ItemsService {
82
82
  const mainHash = objectHash(mainItem);
83
83
  return { outdated: hash !== mainHash, mainHash };
84
84
  }
85
- async getVersionSavesById(id) {
86
- const revisionsService = new RevisionsService({
87
- knex: this.knex,
88
- schema: this.schema,
89
- });
90
- const result = await revisionsService.readByQuery({
91
- filter: { version: { _eq: id } },
92
- });
93
- return result.map((revision) => revision['delta']);
94
- }
95
- // TODO: Remove legacy need to return a version array in subsequent release
96
85
  async getVersionSaves(key, collection, item) {
97
86
  const filter = {
98
87
  key: { _eq: key },
@@ -107,8 +96,7 @@ export class VersionsService extends ItemsService {
107
96
  if (versions[0]['delta']) {
108
97
  return [versions[0]['delta']];
109
98
  }
110
- const saves = await this.getVersionSavesById(versions[0]['id']);
111
- return saves;
99
+ return null;
112
100
  }
113
101
  async createOne(data, opts) {
114
102
  await this.validateCreateData(data);
@@ -202,12 +190,7 @@ export class VersionsService extends ItemsService {
202
190
  data: revisionDelta,
203
191
  delta: revisionDelta,
204
192
  });
205
- let existingDelta = version['delta'];
206
- if (!existingDelta) {
207
- const saves = await this.getVersionSavesById(key);
208
- existingDelta = assign({}, ...saves);
209
- }
210
- const finalVersionDelta = assign({}, existingDelta, revisionDelta ? JSON.parse(revisionDelta) : null);
193
+ const finalVersionDelta = assign({}, version['delta'], revisionDelta ? JSON.parse(revisionDelta) : null);
211
194
  const sudoService = new ItemsService(this.collection, {
212
195
  knex: this.knex,
213
196
  schema: this.schema,
@@ -220,7 +203,7 @@ export class VersionsService extends ItemsService {
220
203
  return finalVersionDelta;
221
204
  }
222
205
  async promote(version, mainHash, fields) {
223
- const { id, collection, item, delta } = (await this.readOne(version));
206
+ const { collection, item, delta } = (await this.readOne(version));
224
207
  // will throw an error if the accountability does not have permission to update the item
225
208
  if (this.accountability) {
226
209
  await validateAccess({
@@ -233,21 +216,18 @@ export class VersionsService extends ItemsService {
233
216
  knex: this.knex,
234
217
  });
235
218
  }
219
+ if (!delta) {
220
+ throw new UnprocessableContentError({
221
+ reason: `No changes to promote`,
222
+ });
223
+ }
236
224
  const { outdated } = await this.verifyHash(collection, item, mainHash);
237
225
  if (outdated) {
238
226
  throw new UnprocessableContentError({
239
227
  reason: `Main item has changed since this version was last updated`,
240
228
  });
241
229
  }
242
- let versionResult;
243
- if (delta) {
244
- versionResult = delta;
245
- }
246
- else {
247
- const saves = await this.getVersionSavesById(id);
248
- versionResult = assign({}, ...saves);
249
- }
250
- const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
230
+ const payloadToUpdate = fields ? pick(delta, fields) : delta;
251
231
  const itemsService = new ItemsService(collection, {
252
232
  accountability: this.accountability,
253
233
  knex: this.knex,
@@ -1,4 +1,3 @@
1
1
  export * from './lib/get-report.js';
2
- export * from './lib/init-telemetry.js';
3
2
  export * from './lib/send-report.js';
4
3
  export * from './lib/track.js';
@@ -1,4 +1,3 @@
1
1
  export * from './lib/get-report.js';
2
- export * from './lib/init-telemetry.js';
3
2
  export * from './lib/send-report.js';
4
3
  export * from './lib/track.js';
@@ -148,7 +148,7 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
148
148
  }
149
149
  }
150
150
  }
151
- const fieldsService = new FieldsService({
151
+ let fieldsService = new FieldsService({
152
152
  knex: trx,
153
153
  schema: await getSchema({ database: trx, bypassCache: true }),
154
154
  });
@@ -156,6 +156,11 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
156
156
  if (diff?.[0]?.kind === DiffKind.NEW && !isNestedMetaUpdate(diff?.[0])) {
157
157
  try {
158
158
  await fieldsService.createField(collection, diff[0].rhs, undefined, mutationOptions);
159
+ // Refresh the schema
160
+ fieldsService = new FieldsService({
161
+ knex: trx,
162
+ schema: await getSchema({ database: trx, bypassCache: true }),
163
+ });
159
164
  }
160
165
  catch (err) {
161
166
  logger.error(`Failed to create field "${collection}.${field}"`);
@@ -183,14 +188,21 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
183
188
  if (diff?.[0]?.kind === DiffKind.DELETE && !isNestedMetaUpdate(diff?.[0])) {
184
189
  try {
185
190
  await fieldsService.deleteField(collection, field, mutationOptions);
191
+ // Refresh the schema
192
+ fieldsService = new FieldsService({
193
+ knex: trx,
194
+ schema: await getSchema({ database: trx, bypassCache: true }),
195
+ });
186
196
  }
187
197
  catch (err) {
188
198
  logger.error(`Failed to delete field "${collection}.${field}"`);
189
199
  throw err;
190
200
  }
191
201
  // Field deletion also cleans up the relationship. We should ignore any relationship
192
- // changes attached to this now non-existing field
193
- snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection && relation.field === field) === false);
202
+ // changes attached to this now non-existing field except newly created relationship
203
+ snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection &&
204
+ relation.field === field &&
205
+ !relation.diff.some((diff) => diff.kind === DiffKind.NEW)) === false);
194
206
  }
195
207
  }
196
208
  const relationsService = new RelationsService({
@@ -11,7 +11,7 @@ export function getService(collection, opts) {
11
11
  case 'directus_activity':
12
12
  return new ActivityService(opts);
13
13
  case 'directus_comments':
14
- return new CommentsService({ ...opts, serviceOrigin: 'comments' });
14
+ return new CommentsService(opts);
15
15
  case 'directus_dashboards':
16
16
  return new DashboardsService(opts);
17
17
  case 'directus_files':
@@ -25,6 +25,16 @@ export function getSnapshotDiff(current, after) {
25
25
  ...current.fields.map((currentField) => {
26
26
  const afterField = after.fields.find((afterField) => afterField.collection === currentField.collection && afterField.field === currentField.field);
27
27
  const isAutoIncrementPrimaryKey = !!currentField.schema?.is_primary_key && !!currentField.schema?.has_auto_increment;
28
+ // Changing to/from alias fields should delete the current field
29
+ if (afterField &&
30
+ currentField.type !== afterField.type &&
31
+ (currentField.type === 'alias' || afterField.type === 'alias')) {
32
+ return {
33
+ collection: currentField.collection,
34
+ field: currentField.field,
35
+ diff: deepDiff.diff(sanitizeField(currentField, isAutoIncrementPrimaryKey), sanitizeField(undefined, isAutoIncrementPrimaryKey)),
36
+ };
37
+ }
28
38
  return {
29
39
  collection: currentField.collection,
30
40
  field: currentField.field,
@@ -33,7 +43,13 @@ export function getSnapshotDiff(current, after) {
33
43
  }),
34
44
  ...after.fields
35
45
  .filter((afterField) => {
36
- const currentField = current.fields.find((currentField) => currentField.collection === afterField.collection && afterField.field === currentField.field);
46
+ let currentField = current.fields.find((currentField) => currentField.collection === afterField.collection && afterField.field === currentField.field);
47
+ // Changing to/from alias fields should create the new field
48
+ if (currentField &&
49
+ currentField.type !== afterField.type &&
50
+ (currentField.type === 'alias' || afterField.type === 'alias')) {
51
+ currentField = undefined;
52
+ }
37
53
  return !!currentField === false;
38
54
  })
39
55
  .map((afterField) => ({
@@ -16,6 +16,7 @@ import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js
16
16
  import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
17
17
  import { getMessageType } from '../utils/message.js';
18
18
  import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
19
+ import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
19
20
  const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
20
21
  const logger = useLogger();
21
22
  export default class SocketController {
@@ -116,7 +117,7 @@ export default class SocketController {
116
117
  }
117
118
  this.server.handleUpgrade(request, socket, head, async (ws) => {
118
119
  this.catchInvalidMessages(ws);
119
- const state = { accountability: null, expires_at: null };
120
+ const state = { accountability: createDefaultAccountability(), expires_at: null };
120
121
  this.server.emit('connection', ws, state);
121
122
  });
122
123
  }