@directus/api 23.1.3 → 23.2.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.
- package/dist/app.js +7 -4
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/controllers/activity.js +2 -88
- package/dist/controllers/comments.js +0 -7
- package/dist/controllers/tus.d.ts +0 -1
- package/dist/controllers/tus.js +0 -16
- package/dist/controllers/versions.js +1 -8
- package/dist/database/migrations/20240909A-separate-comments.js +1 -6
- package/dist/database/migrations/20240924A-migrate-legacy-comments.d.ts +3 -0
- package/dist/database/migrations/20240924A-migrate-legacy-comments.js +59 -0
- package/dist/database/migrations/20240924B-populate-versioning-deltas.d.ts +3 -0
- package/dist/database/migrations/20240924B-populate-versioning-deltas.js +32 -0
- package/dist/database/run-ast/utils/apply-parent-filters.js +4 -0
- package/dist/schedules/retention.d.ts +14 -0
- package/dist/schedules/retention.js +96 -0
- package/dist/{telemetry/lib/init-telemetry.d.ts → schedules/telemetry.d.ts} +2 -2
- package/dist/{telemetry/lib/init-telemetry.js → schedules/telemetry.js} +6 -6
- package/dist/schedules/tus.d.ts +6 -0
- package/dist/schedules/tus.js +23 -0
- package/dist/services/assets.js +4 -3
- package/dist/services/comments.d.ts +4 -22
- package/dist/services/comments.js +16 -252
- package/dist/services/graphql/index.d.ts +1 -2
- package/dist/services/graphql/index.js +1 -75
- package/dist/services/users.js +1 -1
- package/dist/services/versions.d.ts +0 -1
- package/dist/services/versions.js +9 -29
- package/dist/telemetry/index.d.ts +0 -1
- package/dist/telemetry/index.js +0 -1
- package/dist/utils/apply-diff.js +15 -3
- package/dist/utils/get-service.js +1 -1
- package/dist/utils/get-snapshot-diff.js +17 -1
- package/dist/websocket/controllers/base.js +2 -1
- package/dist/websocket/controllers/graphql.js +2 -1
- package/package.json +17 -17
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
353
|
-
if (!
|
|
354
|
-
|
|
355
|
-
|
|
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,
|
|
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,
|
|
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: {
|
package/dist/services/users.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
package/dist/telemetry/index.js
CHANGED
package/dist/utils/apply-diff.js
CHANGED
|
@@ -148,7 +148,7 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
|
|
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 &&
|
|
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(
|
|
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
|
-
|
|
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:
|
|
120
|
+
const state = { accountability: createDefaultAccountability(), expires_at: null };
|
|
120
121
|
this.server.emit('connection', ws, state);
|
|
121
122
|
});
|
|
122
123
|
}
|
|
@@ -10,6 +10,7 @@ import { ConnectionParams, WebSocketMessage } from '../messages.js';
|
|
|
10
10
|
import { getMessageType } from '../utils/message.js';
|
|
11
11
|
import SocketController from './base.js';
|
|
12
12
|
import { registerWebSocketEvents } from './hooks.js';
|
|
13
|
+
import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
|
|
13
14
|
const logger = useLogger();
|
|
14
15
|
export class GraphQLSubscriptionController extends SocketController {
|
|
15
16
|
gql;
|
|
@@ -95,7 +96,7 @@ export class GraphQLSubscriptionController extends SocketController {
|
|
|
95
96
|
}
|
|
96
97
|
async handleHandshakeUpgrade({ request, socket, head }) {
|
|
97
98
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
98
|
-
this.server.emit('connection', ws, { accountability:
|
|
99
|
+
this.server.emit('connection', ws, { accountability: createDefaultAccountability(), expires_at: null });
|
|
99
100
|
// actual enforcement is handled by the setTokenExpireTimer function
|
|
100
101
|
});
|
|
101
102
|
}
|