@directus/api 23.1.2 → 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.
Files changed (52) 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/items.js +1 -1
  6. package/dist/controllers/tus.d.ts +0 -1
  7. package/dist/controllers/tus.js +0 -16
  8. package/dist/controllers/versions.js +1 -8
  9. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/cockroachdb.js +3 -3
  11. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/mssql.js +3 -3
  13. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  14. package/dist/database/helpers/schema/dialects/oracle.js +8 -3
  15. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  16. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  17. package/dist/database/helpers/schema/types.d.ts +2 -1
  18. package/dist/database/helpers/schema/types.js +4 -1
  19. package/dist/database/helpers/schema/utils/{preprocess-bindings.d.ts → prep-query-params.d.ts} +2 -2
  20. package/dist/database/helpers/schema/utils/{preprocess-bindings.js → prep-query-params.js} +1 -1
  21. package/dist/database/index.js +8 -2
  22. package/dist/database/migrations/20240909A-separate-comments.js +1 -6
  23. package/dist/database/migrations/20240924A-migrate-legacy-comments.d.ts +3 -0
  24. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +59 -0
  25. package/dist/database/migrations/20240924B-populate-versioning-deltas.d.ts +3 -0
  26. package/dist/database/migrations/20240924B-populate-versioning-deltas.js +32 -0
  27. package/dist/database/run-ast/utils/apply-parent-filters.js +4 -0
  28. package/dist/database/run-ast/utils/with-preprocess-bindings.js +4 -3
  29. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +2 -1
  30. package/dist/schedules/retention.d.ts +14 -0
  31. package/dist/schedules/retention.js +96 -0
  32. package/dist/{telemetry/lib/init-telemetry.d.ts → schedules/telemetry.d.ts} +2 -2
  33. package/dist/{telemetry/lib/init-telemetry.js → schedules/telemetry.js} +6 -6
  34. package/dist/schedules/tus.d.ts +6 -0
  35. package/dist/schedules/tus.js +23 -0
  36. package/dist/services/assets.js +4 -3
  37. package/dist/services/comments.d.ts +4 -22
  38. package/dist/services/comments.js +16 -252
  39. package/dist/services/graphql/index.d.ts +1 -2
  40. package/dist/services/graphql/index.js +1 -75
  41. package/dist/services/users.js +1 -1
  42. package/dist/services/versions.d.ts +0 -1
  43. package/dist/services/versions.js +9 -29
  44. package/dist/storage/register-locations.js +1 -0
  45. package/dist/telemetry/index.d.ts +0 -1
  46. package/dist/telemetry/index.js +0 -1
  47. package/dist/utils/apply-diff.js +15 -3
  48. package/dist/utils/get-service.js +1 -1
  49. package/dist/utils/get-snapshot-diff.js +17 -1
  50. package/dist/websocket/controllers/base.js +2 -1
  51. package/dist/websocket/controllers/graphql.js +2 -1
  52. package/package.json +19 -19
@@ -0,0 +1,14 @@
1
+ import type { Knex } from 'knex';
2
+ export interface RetentionTask {
3
+ collection: string;
4
+ where?: readonly [string, string, Knex.Value | null];
5
+ join?: readonly [string, string, string];
6
+ timeframe: number | undefined;
7
+ }
8
+ export declare function handleRetentionJob(): Promise<void>;
9
+ /**
10
+ * Schedule the retention tracking
11
+ *
12
+ * @returns Whether or not retention has been initialized
13
+ */
14
+ export default function schedule(): Promise<boolean>;
@@ -0,0 +1,96 @@
1
+ import { Action } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
3
+ import { toBoolean } from '@directus/utils';
4
+ import getDatabase from '../database/index.js';
5
+ import { useLock } from '../lock/index.js';
6
+ import { useLogger } from '../logger/index.js';
7
+ import { getMilliseconds } from '../utils/get-milliseconds.js';
8
+ import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
9
+ const env = useEnv();
10
+ const retentionLockKey = 'schedule--data-retention';
11
+ const retentionLockTimeout = 10 * 60 * 1000; // 10 mins
12
+ const ACTIVITY_RETENTION_TIMEFRAME = getMilliseconds(env['ACTIVITY_RETENTION']);
13
+ const FLOW_LOGS_RETENTION_TIMEFRAME = getMilliseconds(env['FLOW_LOGS_RETENTION']);
14
+ const REVISIONS_RETENTION_TIMEFRAME = getMilliseconds(env['REVISIONS_RETENTION']);
15
+ const retentionTasks = [
16
+ {
17
+ collection: 'directus_activity',
18
+ where: ['action', '!=', Action.RUN],
19
+ timeframe: ACTIVITY_RETENTION_TIMEFRAME,
20
+ },
21
+ {
22
+ collection: 'directus_activity',
23
+ where: ['action', '=', Action.RUN],
24
+ timeframe: FLOW_LOGS_RETENTION_TIMEFRAME,
25
+ },
26
+ ];
27
+ export async function handleRetentionJob() {
28
+ const database = getDatabase();
29
+ const logger = useLogger();
30
+ const lock = useLock();
31
+ const batch = Number(env['RETENTION_BATCH']);
32
+ const lockTime = await lock.get(retentionLockKey);
33
+ const now = Date.now();
34
+ if (lockTime && Number(lockTime) > now - retentionLockTimeout) {
35
+ // ensure only one connected process
36
+ return;
37
+ }
38
+ await lock.set(retentionLockKey, Date.now());
39
+ for (const task of retentionTasks) {
40
+ let count = 0;
41
+ if (task.timeframe === undefined) {
42
+ // skip disabled tasks
43
+ continue;
44
+ }
45
+ do {
46
+ const subquery = database
47
+ .queryBuilder()
48
+ .select(`${task.collection}.id`)
49
+ .from(task.collection)
50
+ .where('timestamp', '<', Date.now() - task.timeframe)
51
+ .limit(batch);
52
+ if (task.where) {
53
+ subquery.where(...task.where);
54
+ }
55
+ if (task.join) {
56
+ subquery.join(...task.join);
57
+ }
58
+ try {
59
+ count = await database(task.collection).where('id', 'in', subquery).delete();
60
+ }
61
+ catch (error) {
62
+ logger.error(error, `Retention failed for Collection ${task.collection}`);
63
+ break;
64
+ }
65
+ // Update lock time to prevent concurrent runs
66
+ await lock.set(retentionLockKey, Date.now());
67
+ } while (count >= batch);
68
+ }
69
+ await lock.delete(retentionLockKey);
70
+ }
71
+ /**
72
+ * Schedule the retention tracking
73
+ *
74
+ * @returns Whether or not retention has been initialized
75
+ */
76
+ export default async function schedule() {
77
+ const env = useEnv();
78
+ if (!toBoolean(env['RETENTION_ENABLED'])) {
79
+ return false;
80
+ }
81
+ if (!validateCron(String(env['RETENTION_SCHEDULE']))) {
82
+ return false;
83
+ }
84
+ if (!ACTIVITY_RETENTION_TIMEFRAME ||
85
+ (ACTIVITY_RETENTION_TIMEFRAME &&
86
+ REVISIONS_RETENTION_TIMEFRAME &&
87
+ ACTIVITY_RETENTION_TIMEFRAME > REVISIONS_RETENTION_TIMEFRAME)) {
88
+ retentionTasks.push({
89
+ collection: 'directus_revisions',
90
+ join: ['directus_activity', 'directus_revisions.activity', 'directus_activity.id'],
91
+ timeframe: REVISIONS_RETENTION_TIMEFRAME,
92
+ });
93
+ }
94
+ scheduleSynchronizedJob('retention', String(env['RETENTION_SCHEDULE']), handleRetentionJob);
95
+ return true;
96
+ }
@@ -3,9 +3,9 @@
3
3
  */
4
4
  export declare const jobCallback: () => void;
5
5
  /**
6
- * Initialize the telemetry tracking. Will generate a report on start, and set a schedule to report
6
+ * Schedule the telemetry tracking. Will generate a report on start, and set a schedule to report
7
7
  * every 6 hours
8
8
  *
9
9
  * @returns Whether or not telemetry has been initialized
10
10
  */
11
- export declare const initTelemetry: () => Promise<boolean>;
11
+ export default function schedule(): Promise<boolean>;
@@ -1,8 +1,8 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { toBoolean } from '@directus/utils';
3
- import { getCache } from '../../cache.js';
4
- import { scheduleSynchronizedJob } from '../../utils/schedule.js';
5
- import { track } from './track.js';
3
+ import { getCache } from '../cache.js';
4
+ import { scheduleSynchronizedJob } from '../utils/schedule.js';
5
+ import { track } from '../telemetry/index.js';
6
6
  /**
7
7
  * Exported to be able to test the anonymous callback function
8
8
  */
@@ -10,12 +10,12 @@ export const jobCallback = () => {
10
10
  track();
11
11
  };
12
12
  /**
13
- * Initialize the telemetry tracking. Will generate a report on start, and set a schedule to report
13
+ * Schedule the telemetry tracking. Will generate a report on start, and set a schedule to report
14
14
  * every 6 hours
15
15
  *
16
16
  * @returns Whether or not telemetry has been initialized
17
17
  */
18
- export const initTelemetry = async () => {
18
+ export default async function schedule() {
19
19
  const env = useEnv();
20
20
  if (toBoolean(env['TELEMETRY']) === false)
21
21
  return false;
@@ -27,4 +27,4 @@ export const initTelemetry = async () => {
27
27
  // Don't flush the lock. We want to debounce these calls across containers on startup
28
28
  }
29
29
  return true;
30
- };
30
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Schedule the tus cleanup
3
+ *
4
+ * @returns Whether or not tus cleanup has been initialized
5
+ */
6
+ export default function schedule(): Promise<boolean>;
@@ -0,0 +1,23 @@
1
+ import { RESUMABLE_UPLOADS } from '../constants.js';
2
+ import { getSchema } from '../utils/get-schema.js';
3
+ import { createTusServer } from '../services/tus/index.js';
4
+ import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
5
+ /**
6
+ * Schedule the tus cleanup
7
+ *
8
+ * @returns Whether or not tus cleanup has been initialized
9
+ */
10
+ export default async function schedule() {
11
+ if (!RESUMABLE_UPLOADS.ENABLED)
12
+ return false;
13
+ if (validateCron(RESUMABLE_UPLOADS.SCHEDULE)) {
14
+ scheduleSynchronizedJob('tus-cleanup', RESUMABLE_UPLOADS.SCHEDULE, async () => {
15
+ const [tusServer, cleanupServer] = await createTusServer({
16
+ schema: await getSchema(),
17
+ });
18
+ await tusServer.cleanUpExpiredUploads();
19
+ cleanupServer();
20
+ });
21
+ }
22
+ return true;
23
+ }
@@ -87,6 +87,8 @@ export class AssetsService {
87
87
  }
88
88
  const type = file.type;
89
89
  const transforms = transformation ? TransformationUtils.resolvePreset(transformation, file) : [];
90
+ const modifiedOn = file.modified_on ? new Date(file.modified_on) : undefined;
91
+ const version = modifiedOn ? (modifiedOn.getTime() / 1000).toFixed() : undefined;
90
92
  if (type && transforms.length > 0 && SUPPORTED_IMAGE_TRANSFORM_FORMATS.includes(type)) {
91
93
  const maybeNewFormat = TransformationUtils.maybeExtractFormat(transforms);
92
94
  const assetFilename = path.basename(file.filename_disk, path.extname(file.filename_disk)) +
@@ -121,7 +123,6 @@ export class AssetsService {
121
123
  reason: 'Server too busy',
122
124
  });
123
125
  }
124
- const version = file.modified_on !== undefined ? String(Math.round(new Date(file.modified_on).getTime() / 1000)) : undefined;
125
126
  const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
126
127
  const transformer = getSharpInstance();
127
128
  transformer.timeout({
@@ -152,13 +153,13 @@ export class AssetsService {
152
153
  }
153
154
  }
154
155
  return {
155
- stream: await storage.location(file.storage).read(assetFilename, { range }),
156
+ stream: await storage.location(file.storage).read(assetFilename, { range, version }),
156
157
  stat: await storage.location(file.storage).stat(assetFilename),
157
158
  file,
158
159
  };
159
160
  }
160
161
  else {
161
- const readStream = await storage.location(file.storage).read(file.filename_disk, { range });
162
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
162
163
  const stat = await storage.location(file.storage).stat(file.filename_disk);
163
164
  return { stream: readStream, file, stat };
164
165
  }
@@ -1,31 +1,13 @@
1
- import type { Comment, Item, PrimaryKey, Query } from '@directus/types';
1
+ import type { Comment, PrimaryKey } from '@directus/types';
2
2
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
- import { ActivityService } from './activity.js';
4
- import { ItemsService, type QueryOptions } from './items.js';
3
+ import { ItemsService } from './items.js';
5
4
  import { NotificationsService } from './notifications.js';
6
5
  import { UsersService } from './users.js';
7
- type serviceOrigin = 'activity' | 'comments';
8
6
  export declare class CommentsService extends ItemsService {
9
- activityService: ActivityService;
10
7
  notificationsService: NotificationsService;
11
8
  usersService: UsersService;
12
- serviceOrigin: serviceOrigin;
13
- constructor(options: AbstractServiceOptions & {
14
- serviceOrigin: serviceOrigin;
15
- });
16
- readOne(key: PrimaryKey, query?: Query, opts?: QueryOptions): Promise<Item>;
17
- readByQuery(query: Query, opts?: QueryOptions): Promise<Item[]>;
18
- readMany(keys: PrimaryKey[], query?: Query, opts?: QueryOptions): Promise<Item[]>;
9
+ constructor(options: AbstractServiceOptions);
19
10
  createOne(data: Partial<Comment>, opts?: MutationOptions): Promise<PrimaryKey>;
20
- updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
21
- updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
22
- updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
23
- deleteByQuery(query: Query, opts?: MutationOptions): Promise<PrimaryKey[]>;
24
- deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
11
+ updateOne(key: PrimaryKey, data: Partial<Comment>, opts?: MutationOptions): Promise<PrimaryKey>;
25
12
  deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey>;
26
- private processPrimaryKeys;
27
- migrateLegacyComment(activityPk: PrimaryKey): Promise<PrimaryKey>;
28
- generateQuery(type: serviceOrigin, originalQuery: Query): Query;
29
- private sortLegacyResults;
30
13
  }
31
- export {};
@@ -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;