@directus/api 23.0.0 → 23.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +2 -0
- package/dist/controllers/activity.js +30 -27
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/comments.d.ts +2 -0
- package/dist/controllers/comments.js +153 -0
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/users.js +4 -8
- package/dist/controllers/versions.js +10 -5
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -2
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mssql.js +1 -1
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mysql.js +2 -2
- package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/oracle.js +1 -1
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/postgres.js +3 -3
- package/dist/database/helpers/schema/types.d.ts +1 -1
- package/dist/database/helpers/schema/types.js +1 -1
- package/dist/database/index.js +3 -0
- package/dist/database/migrations/20240806A-permissions-policies.d.ts +0 -3
- package/dist/database/migrations/20240806A-permissions-policies.js +8 -94
- package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
- package/dist/database/migrations/20240909A-separate-comments.js +65 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
- package/dist/database/run-ast/lib/get-db-query.js +3 -3
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
- package/dist/database/run-ast/run-ast.js +8 -1
- package/dist/database/run-ast/utils/apply-case-when.js +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
- package/dist/permissions/lib/fetch-permissions.d.ts +1 -1
- package/dist/permissions/lib/fetch-permissions.js +4 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
- package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
- package/dist/permissions/modules/validate-access/validate-access.js +14 -1
- package/dist/permissions/utils/fetch-share-info.d.ts +12 -0
- package/dist/permissions/utils/fetch-share-info.js +9 -0
- package/dist/permissions/utils/get-permissions-for-share.d.ts +4 -0
- package/dist/permissions/utils/get-permissions-for-share.js +182 -0
- package/dist/permissions/utils/merge-permissions.d.ts +9 -0
- package/dist/permissions/utils/merge-permissions.js +118 -0
- package/dist/services/activity.d.ts +1 -7
- package/dist/services/activity.js +0 -103
- package/dist/services/assets.js +5 -4
- package/dist/services/authentication.js +1 -10
- package/dist/services/collections.js +6 -4
- package/dist/services/comments.d.ts +31 -0
- package/dist/services/comments.js +378 -0
- package/dist/services/graphql/index.js +17 -16
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +3 -1
- package/dist/services/mail/index.d.ts +2 -1
- package/dist/services/mail/index.js +4 -1
- package/dist/services/payload.js +15 -14
- package/dist/services/shares.d.ts +2 -0
- package/dist/services/shares.js +11 -9
- package/dist/services/users.js +1 -0
- package/dist/services/versions.js +59 -44
- package/dist/types/auth.d.ts +0 -7
- package/dist/utils/apply-diff.js +5 -6
- package/dist/utils/get-accountability-for-token.js +0 -2
- package/dist/utils/get-service.js +3 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/sanitize-schema.js +2 -0
- package/package.json +52 -52
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { Action } from '@directus/constants';
|
|
2
|
+
import { useEnv } from '@directus/env';
|
|
3
|
+
import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
|
|
4
|
+
import { cloneDeep, mergeWith, uniq } from 'lodash-es';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { useLogger } from '../logger/index.js';
|
|
7
|
+
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
8
|
+
import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
|
|
9
|
+
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
10
|
+
import { isValidUuid } from '../utils/is-valid-uuid.js';
|
|
11
|
+
import { transaction } from '../utils/transaction.js';
|
|
12
|
+
import { Url } from '../utils/url.js';
|
|
13
|
+
import { userName } from '../utils/user-name.js';
|
|
14
|
+
import { ActivityService } from './activity.js';
|
|
15
|
+
import { ItemsService } from './items.js';
|
|
16
|
+
import { NotificationsService } from './notifications.js';
|
|
17
|
+
import { UsersService } from './users.js';
|
|
18
|
+
const env = useEnv();
|
|
19
|
+
const logger = useLogger();
|
|
20
|
+
// TODO: Remove legacy comments logic
|
|
21
|
+
export class CommentsService extends ItemsService {
|
|
22
|
+
activityService;
|
|
23
|
+
notificationsService;
|
|
24
|
+
usersService;
|
|
25
|
+
serviceOrigin;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super('directus_comments', options);
|
|
28
|
+
this.activityService = new ActivityService(options);
|
|
29
|
+
this.notificationsService = new NotificationsService({ schema: this.schema });
|
|
30
|
+
this.usersService = new UsersService({ schema: this.schema });
|
|
31
|
+
this.serviceOrigin = options.serviceOrigin ?? 'comments';
|
|
32
|
+
}
|
|
33
|
+
readOne(key, query, opts) {
|
|
34
|
+
const isLegacyComment = isNaN(Number(key));
|
|
35
|
+
let result;
|
|
36
|
+
if (isLegacyComment) {
|
|
37
|
+
const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
|
|
38
|
+
result = this.activityService.readOne(key, activityQuery, opts);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
|
|
42
|
+
result = super.readOne(key, commentsQuery, opts);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async readByQuery(query, opts) {
|
|
47
|
+
const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query);
|
|
48
|
+
const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query);
|
|
49
|
+
const activityResult = await this.activityService.readByQuery(activityQuery, opts);
|
|
50
|
+
const commentsResult = await super.readByQuery(commentsQuery, opts);
|
|
51
|
+
if (query.aggregate) {
|
|
52
|
+
// Merging the first result only as the app does not utilise group
|
|
53
|
+
return [
|
|
54
|
+
mergeWith({}, activityResult[0], commentsResult[0], (a, b) => {
|
|
55
|
+
const numA = Number(a);
|
|
56
|
+
const numB = Number(b);
|
|
57
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
58
|
+
return numA + numB;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
else if (query.sort) {
|
|
65
|
+
return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return [...activityResult, ...commentsResult];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async readMany(keys, query, opts) {
|
|
72
|
+
const commentsKeys = [];
|
|
73
|
+
const activityKeys = [];
|
|
74
|
+
for (const key of keys) {
|
|
75
|
+
if (isNaN(Number(key))) {
|
|
76
|
+
commentsKeys.push(key);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
activityKeys.push(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const activityQuery = this.serviceOrigin === 'activity' ? query : this.generateQuery('activity', query || {});
|
|
83
|
+
const commentsQuery = this.serviceOrigin === 'comments' ? query : this.generateQuery('comments', query || {});
|
|
84
|
+
const activityResult = await this.activityService.readMany(activityKeys, activityQuery, opts);
|
|
85
|
+
const commentsResult = await super.readMany(commentsKeys, commentsQuery, opts);
|
|
86
|
+
if (query?.sort) {
|
|
87
|
+
return this.sortLegacyResults([...activityResult, ...commentsResult], query.sort);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return [...activityResult, ...commentsResult];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async createOne(data, opts) {
|
|
94
|
+
if (!data['comment']) {
|
|
95
|
+
throw new InvalidPayloadError({ reason: `"comment" is required` });
|
|
96
|
+
}
|
|
97
|
+
if (!data['collection']) {
|
|
98
|
+
throw new InvalidPayloadError({ reason: `"collection" is required` });
|
|
99
|
+
}
|
|
100
|
+
if (!data['item']) {
|
|
101
|
+
throw new InvalidPayloadError({ reason: `"item" is required` });
|
|
102
|
+
}
|
|
103
|
+
if (this.accountability) {
|
|
104
|
+
await validateAccess({
|
|
105
|
+
accountability: this.accountability,
|
|
106
|
+
action: 'read',
|
|
107
|
+
collection: data['collection'],
|
|
108
|
+
primaryKeys: [data['item']],
|
|
109
|
+
}, {
|
|
110
|
+
schema: this.schema,
|
|
111
|
+
knex: this.knex,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
|
|
115
|
+
const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
|
|
116
|
+
const sender = await this.usersService.readOne(this.accountability.user, {
|
|
117
|
+
fields: ['id', 'first_name', 'last_name', 'email'],
|
|
118
|
+
});
|
|
119
|
+
for (const mention of mentions) {
|
|
120
|
+
const userID = mention.substring(1);
|
|
121
|
+
const user = await this.usersService.readOne(userID, {
|
|
122
|
+
fields: ['id', 'first_name', 'last_name', 'email', 'role.id'],
|
|
123
|
+
});
|
|
124
|
+
const accountability = {
|
|
125
|
+
user: userID,
|
|
126
|
+
role: user['role']?.id ?? null,
|
|
127
|
+
admin: false,
|
|
128
|
+
app: false,
|
|
129
|
+
roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
|
|
130
|
+
ip: null,
|
|
131
|
+
};
|
|
132
|
+
const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
|
|
133
|
+
accountability.admin = userGlobalAccess.admin;
|
|
134
|
+
accountability.app = userGlobalAccess.app;
|
|
135
|
+
const usersService = new UsersService({ schema: this.schema, accountability });
|
|
136
|
+
try {
|
|
137
|
+
await validateAccess({
|
|
138
|
+
accountability,
|
|
139
|
+
action: 'read',
|
|
140
|
+
collection: data['collection'],
|
|
141
|
+
primaryKeys: [data['item']],
|
|
142
|
+
}, {
|
|
143
|
+
schema: this.schema,
|
|
144
|
+
knex: this.knex,
|
|
145
|
+
});
|
|
146
|
+
const templateData = await usersService.readByQuery({
|
|
147
|
+
fields: ['id', 'first_name', 'last_name', 'email'],
|
|
148
|
+
filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
|
|
149
|
+
});
|
|
150
|
+
const userPreviews = templateData.reduce((acc, user) => {
|
|
151
|
+
acc[user['id']] = `<em>${userName(user)}</em>`;
|
|
152
|
+
return acc;
|
|
153
|
+
}, {});
|
|
154
|
+
let comment = data['comment'];
|
|
155
|
+
for (const mention of mentions) {
|
|
156
|
+
const uuid = mention.substring(1);
|
|
157
|
+
// We only match on UUIDs in the first place. This is just an extra sanity check.
|
|
158
|
+
if (isValidUuid(uuid) === false)
|
|
159
|
+
continue;
|
|
160
|
+
comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
|
|
161
|
+
}
|
|
162
|
+
comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
|
|
163
|
+
const href = new Url(env['PUBLIC_URL'])
|
|
164
|
+
.addPath('admin', 'content', data['collection'], data['item'])
|
|
165
|
+
.toString();
|
|
166
|
+
const message = `
|
|
167
|
+
Hello ${userName(user)},
|
|
168
|
+
|
|
169
|
+
${userName(sender)} has mentioned you in a comment:
|
|
170
|
+
|
|
171
|
+
${comment}
|
|
172
|
+
|
|
173
|
+
<a href="${href}">Click here to view.</a>
|
|
174
|
+
`;
|
|
175
|
+
await this.notificationsService.createOne({
|
|
176
|
+
recipient: userID,
|
|
177
|
+
sender: sender['id'],
|
|
178
|
+
subject: `You were mentioned in ${data['collection']}`,
|
|
179
|
+
message,
|
|
180
|
+
collection: data['collection'],
|
|
181
|
+
item: data['item'],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (isDirectusError(err, ErrorCode.Forbidden)) {
|
|
186
|
+
logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return super.createOne(data, opts);
|
|
194
|
+
}
|
|
195
|
+
async updateByQuery(query, data, opts) {
|
|
196
|
+
const keys = await this.getKeysByQuery(query);
|
|
197
|
+
const migratedKeys = await this.processPrimaryKeys(keys);
|
|
198
|
+
return super.updateMany(migratedKeys, data, opts);
|
|
199
|
+
}
|
|
200
|
+
async updateMany(keys, data, opts) {
|
|
201
|
+
const migratedKeys = await this.processPrimaryKeys(keys);
|
|
202
|
+
return super.updateMany(migratedKeys, data, opts);
|
|
203
|
+
}
|
|
204
|
+
async updateOne(key, data, opts) {
|
|
205
|
+
const migratedKey = await this.migrateLegacyComment(key);
|
|
206
|
+
return super.updateOne(migratedKey, data, opts);
|
|
207
|
+
}
|
|
208
|
+
async deleteByQuery(query, opts) {
|
|
209
|
+
const keys = await this.getKeysByQuery(query);
|
|
210
|
+
const migratedKeys = await this.processPrimaryKeys(keys);
|
|
211
|
+
return super.deleteMany(migratedKeys, opts);
|
|
212
|
+
}
|
|
213
|
+
async deleteMany(keys, opts) {
|
|
214
|
+
const migratedKeys = await this.processPrimaryKeys(keys);
|
|
215
|
+
return super.deleteMany(migratedKeys, opts);
|
|
216
|
+
}
|
|
217
|
+
async deleteOne(key, opts) {
|
|
218
|
+
const migratedKey = await this.migrateLegacyComment(key);
|
|
219
|
+
return super.deleteOne(migratedKey, opts);
|
|
220
|
+
}
|
|
221
|
+
async processPrimaryKeys(keys) {
|
|
222
|
+
const migratedKeys = [];
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
if (isNaN(Number(key))) {
|
|
225
|
+
migratedKeys.push(key);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
migratedKeys.push(await this.migrateLegacyComment(key));
|
|
229
|
+
}
|
|
230
|
+
return migratedKeys;
|
|
231
|
+
}
|
|
232
|
+
async migrateLegacyComment(activityPk) {
|
|
233
|
+
// Skip migration if not a legacy comment
|
|
234
|
+
if (isNaN(Number(activityPk))) {
|
|
235
|
+
return activityPk;
|
|
236
|
+
}
|
|
237
|
+
return transaction(this.knex, async (trx) => {
|
|
238
|
+
let primaryKey;
|
|
239
|
+
const legacyComment = await trx('directus_activity').select('*').where('id', '=', activityPk).first();
|
|
240
|
+
// Legacy comment
|
|
241
|
+
if (legacyComment['action'] === Action.COMMENT) {
|
|
242
|
+
primaryKey = randomUUID();
|
|
243
|
+
await trx('directus_comments').insert({
|
|
244
|
+
id: primaryKey,
|
|
245
|
+
collection: legacyComment.collection,
|
|
246
|
+
item: legacyComment.item,
|
|
247
|
+
comment: legacyComment.comment,
|
|
248
|
+
user_created: legacyComment.user,
|
|
249
|
+
date_created: legacyComment.timestamp,
|
|
250
|
+
});
|
|
251
|
+
await trx('directus_activity')
|
|
252
|
+
.update({
|
|
253
|
+
action: Action.CREATE,
|
|
254
|
+
collection: 'directus_comments',
|
|
255
|
+
item: primaryKey,
|
|
256
|
+
comment: null,
|
|
257
|
+
})
|
|
258
|
+
.where('id', '=', activityPk);
|
|
259
|
+
}
|
|
260
|
+
// Migrated comment
|
|
261
|
+
else if (legacyComment.collection === 'directus_comment' && legacyComment.action === Action.CREATE) {
|
|
262
|
+
primaryKey = legacyComment.item;
|
|
263
|
+
}
|
|
264
|
+
if (!primaryKey) {
|
|
265
|
+
throw new ForbiddenError();
|
|
266
|
+
}
|
|
267
|
+
return primaryKey;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
generateQuery(type, originalQuery) {
|
|
271
|
+
const query = cloneDeep(originalQuery);
|
|
272
|
+
const defaultActivityCommentFilter = { action: { _eq: Action.COMMENT } };
|
|
273
|
+
const commentsToActivityFieldMap = {
|
|
274
|
+
id: 'id',
|
|
275
|
+
comment: 'comment',
|
|
276
|
+
item: 'item',
|
|
277
|
+
collection: 'collection',
|
|
278
|
+
user_created: 'user',
|
|
279
|
+
date_created: 'timestamp',
|
|
280
|
+
};
|
|
281
|
+
const activityToCommentsFieldMap = {
|
|
282
|
+
id: 'id',
|
|
283
|
+
comment: 'comment',
|
|
284
|
+
item: 'item',
|
|
285
|
+
collection: 'collection',
|
|
286
|
+
user: 'user_created',
|
|
287
|
+
timestamp: 'date_created',
|
|
288
|
+
};
|
|
289
|
+
const targetFieldMap = type === 'activity' ? commentsToActivityFieldMap : activityToCommentsFieldMap;
|
|
290
|
+
for (const key of Object.keys(originalQuery)) {
|
|
291
|
+
switch (key) {
|
|
292
|
+
case 'fields':
|
|
293
|
+
if (!originalQuery.fields)
|
|
294
|
+
break;
|
|
295
|
+
query.fields = [];
|
|
296
|
+
for (const field of originalQuery.fields) {
|
|
297
|
+
if (field === '*') {
|
|
298
|
+
query.fields = ['*'];
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
const parts = field.split('.');
|
|
302
|
+
const firstPart = parts[0];
|
|
303
|
+
if (firstPart && targetFieldMap[firstPart]) {
|
|
304
|
+
query.fields.push(field);
|
|
305
|
+
if (firstPart !== targetFieldMap[firstPart]) {
|
|
306
|
+
(query.alias = query.alias || {})[firstPart] = targetFieldMap[firstPart];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case 'filter':
|
|
312
|
+
if (!originalQuery.filter)
|
|
313
|
+
break;
|
|
314
|
+
if (type === 'activity') {
|
|
315
|
+
query.filter = { _and: [defaultActivityCommentFilter, originalQuery.filter] };
|
|
316
|
+
}
|
|
317
|
+
if (type === 'comments' && this.serviceOrigin === 'activity') {
|
|
318
|
+
if ('_and' in originalQuery.filter && Array.isArray(originalQuery.filter['_and'])) {
|
|
319
|
+
query.filter = {
|
|
320
|
+
_and: originalQuery.filter['_and'].filter((andItem) => !('action' in andItem && '_eq' in andItem['action'] && andItem['action']['_eq'] === 'comment')),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
query.filter = originalQuery.filter;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
case 'aggregate':
|
|
329
|
+
if (originalQuery.aggregate) {
|
|
330
|
+
query.aggregate = originalQuery.aggregate;
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
case 'sort':
|
|
334
|
+
if (!originalQuery.sort)
|
|
335
|
+
break;
|
|
336
|
+
query.sort = [];
|
|
337
|
+
for (const sort of originalQuery.sort) {
|
|
338
|
+
const isDescending = sort.startsWith('-');
|
|
339
|
+
const field = isDescending ? sort.slice(1) : sort;
|
|
340
|
+
if (field && targetFieldMap[field]) {
|
|
341
|
+
query.sort.push(`${isDescending ? '-' : ''}${targetFieldMap[field]}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (type === 'activity' && !query.filter) {
|
|
348
|
+
query.filter = defaultActivityCommentFilter;
|
|
349
|
+
}
|
|
350
|
+
return query;
|
|
351
|
+
}
|
|
352
|
+
sortLegacyResults(results, sort) {
|
|
353
|
+
if (!sort)
|
|
354
|
+
return results;
|
|
355
|
+
let sortKeys = sort;
|
|
356
|
+
// Fix legacy app sort query which uses id
|
|
357
|
+
if (sortKeys.length === 1 && sortKeys[0]?.endsWith('id') && results[0]?.['timestamp']) {
|
|
358
|
+
sortKeys = [`${sortKeys[0].startsWith('-') ? '-' : ''}timestamp`];
|
|
359
|
+
}
|
|
360
|
+
return results.sort((a, b) => {
|
|
361
|
+
for (const key of sortKeys) {
|
|
362
|
+
const isDescending = key.startsWith('-');
|
|
363
|
+
const actualKey = isDescending ? key.substring(1) : key;
|
|
364
|
+
let aValue = a[actualKey];
|
|
365
|
+
let bValue = b[actualKey];
|
|
366
|
+
if (actualKey === 'date_created' || actualKey === 'timestamp') {
|
|
367
|
+
aValue = new Date(aValue);
|
|
368
|
+
bValue = new Date(bValue);
|
|
369
|
+
}
|
|
370
|
+
if (aValue < bValue)
|
|
371
|
+
return isDescending ? 1 : -1;
|
|
372
|
+
if (aValue > bValue)
|
|
373
|
+
return isDescending ? -1 : 1;
|
|
374
|
+
}
|
|
375
|
+
return 0;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
2804
|
+
const commentsService = new CommentsService({
|
|
2806
2805
|
accountability: this.accountability,
|
|
2807
2806
|
schema: this.schema,
|
|
2807
|
+
serviceOrigin: 'activity',
|
|
2808
2808
|
});
|
|
2809
|
-
const primaryKey = await
|
|
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
|
|
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
|
|
2828
|
+
const commentsService = new CommentsService({
|
|
2829
2829
|
accountability: this.accountability,
|
|
2830
2830
|
schema: this.schema,
|
|
2831
|
+
serviceOrigin: 'activity',
|
|
2831
2832
|
});
|
|
2832
|
-
await
|
|
2833
|
+
await commentsService.deleteOne(args['id']);
|
|
2833
2834
|
return { id: args['id'] };
|
|
2834
2835
|
},
|
|
2835
2836
|
},
|
package/dist/services/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/services/index.js
CHANGED
|
@@ -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';
|
package/dist/services/items.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 = {
|
package/dist/services/payload.js
CHANGED
|
@@ -3,7 +3,7 @@ import { parseJSON, toArray } from '@directus/utils';
|
|
|
3
3
|
import { format, isValid, parseISO } from 'date-fns';
|
|
4
4
|
import { unflatten } from 'flat';
|
|
5
5
|
import Joi from 'joi';
|
|
6
|
-
import { clone, cloneDeep, isNil, isObject, isPlainObject,
|
|
6
|
+
import { clone, cloneDeep, isNil, isObject, isPlainObject, pick } from 'lodash-es';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { parse as wktToGeoJSON } from 'wellknown';
|
|
9
9
|
import { getHelpers } from '../database/helpers/index.js';
|
|
@@ -347,22 +347,22 @@ export class PayloadService {
|
|
|
347
347
|
knex: this.knex,
|
|
348
348
|
schema: this.schema,
|
|
349
349
|
});
|
|
350
|
-
const
|
|
350
|
+
const relatedPrimaryKeyField = this.schema.collections[relatedCollection].primary;
|
|
351
351
|
const relatedRecord = payload[relation.field];
|
|
352
352
|
if (['string', 'number'].includes(typeof relatedRecord))
|
|
353
353
|
continue;
|
|
354
|
-
const hasPrimaryKey =
|
|
355
|
-
let relatedPrimaryKey = relatedRecord[
|
|
354
|
+
const hasPrimaryKey = relatedPrimaryKeyField in relatedRecord;
|
|
355
|
+
let relatedPrimaryKey = relatedRecord[relatedPrimaryKeyField];
|
|
356
356
|
const exists = hasPrimaryKey &&
|
|
357
357
|
!!(await this.knex
|
|
358
|
-
.select(
|
|
358
|
+
.select(relatedPrimaryKeyField)
|
|
359
359
|
.from(relatedCollection)
|
|
360
|
-
.where({ [
|
|
360
|
+
.where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
|
|
361
361
|
.first());
|
|
362
362
|
if (exists) {
|
|
363
|
-
const
|
|
364
|
-
if (Object.keys(
|
|
365
|
-
await service.updateOne(relatedPrimaryKey,
|
|
363
|
+
const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
|
|
364
|
+
if (Object.keys(record).length > 0) {
|
|
365
|
+
await service.updateOne(relatedPrimaryKey, record, {
|
|
366
366
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
367
367
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
368
368
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
@@ -425,9 +425,9 @@ export class PayloadService {
|
|
|
425
425
|
.where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
|
|
426
426
|
.first());
|
|
427
427
|
if (exists) {
|
|
428
|
-
const
|
|
429
|
-
if (Object.keys(
|
|
430
|
-
await service.updateOne(relatedPrimaryKey,
|
|
428
|
+
const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
|
|
429
|
+
if (Object.keys(record).length > 0) {
|
|
430
|
+
await service.updateOne(relatedPrimaryKey, record, {
|
|
431
431
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
432
432
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
433
433
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
@@ -610,8 +610,9 @@ export class PayloadService {
|
|
|
610
610
|
if (alterations.update) {
|
|
611
611
|
const primaryKeyField = this.schema.collections[relation.collection].primary;
|
|
612
612
|
for (const item of alterations.update) {
|
|
613
|
-
|
|
614
|
-
|
|
613
|
+
const { [primaryKeyField]: key, ...record } = item;
|
|
614
|
+
await service.updateOne(key, {
|
|
615
|
+
...record,
|
|
615
616
|
[relation.field]: parent || payload[currentPrimaryKeyField],
|
|
616
617
|
}, {
|
|
617
618
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
@@ -4,6 +4,8 @@ import { ItemsService } from './items.js';
|
|
|
4
4
|
export declare class SharesService extends ItemsService {
|
|
5
5
|
constructor(options: AbstractServiceOptions);
|
|
6
6
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
7
|
+
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
8
|
+
deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
7
9
|
login(payload: Record<string, any>, options?: Partial<{
|
|
8
10
|
session: boolean;
|
|
9
11
|
}>): Promise<Omit<LoginResult, 'id'>>;
|