@directus/api 22.2.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/drivers/ldap.js +14 -3
  3. package/dist/auth/drivers/oauth2.js +13 -2
  4. package/dist/auth/drivers/openid.js +13 -2
  5. package/dist/cache.js +4 -4
  6. package/dist/cli/commands/init/questions.d.ts +5 -5
  7. package/dist/cli/commands/schema/apply.d.ts +1 -0
  8. package/dist/cli/commands/schema/apply.js +20 -1
  9. package/dist/cli/index.js +1 -0
  10. package/dist/cli/utils/create-env/env-stub.liquid +1 -4
  11. package/dist/controllers/activity.js +30 -27
  12. package/dist/controllers/assets.js +1 -1
  13. package/dist/controllers/comments.d.ts +2 -0
  14. package/dist/controllers/comments.js +153 -0
  15. package/dist/controllers/versions.js +10 -5
  16. package/dist/database/index.js +3 -0
  17. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  18. package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
  19. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  20. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  21. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  22. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  23. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  24. package/dist/database/run-ast/lib/get-db-query.js +2 -2
  25. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  26. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  27. package/dist/database/run-ast/run-ast.js +8 -1
  28. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  29. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  30. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
  31. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
  32. package/dist/logger/index.d.ts +2 -3
  33. package/dist/logger/logs-stream.d.ts +0 -1
  34. package/dist/mailer.js +0 -6
  35. package/dist/middleware/authenticate.d.ts +1 -3
  36. package/dist/middleware/error-handler.d.ts +0 -1
  37. package/dist/middleware/validate-batch.d.ts +1 -4
  38. package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
  39. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
  40. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  41. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  42. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  43. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  44. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
  46. package/dist/permissions/utils/process-permissions.d.ts +11 -1
  47. package/dist/permissions/utils/process-permissions.js +6 -4
  48. package/dist/request/agent-with-ip-validation.d.ts +0 -1
  49. package/dist/server.d.ts +0 -3
  50. package/dist/services/activity.d.ts +1 -7
  51. package/dist/services/activity.js +0 -103
  52. package/dist/services/assets.d.ts +0 -1
  53. package/dist/services/assets.js +5 -4
  54. package/dist/services/collections.js +6 -4
  55. package/dist/services/comments.d.ts +31 -0
  56. package/dist/services/comments.js +374 -0
  57. package/dist/services/fields.js +0 -6
  58. package/dist/services/files/utils/get-metadata.d.ts +0 -1
  59. package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
  60. package/dist/services/files.d.ts +0 -1
  61. package/dist/services/graphql/index.js +17 -16
  62. package/dist/services/import-export.d.ts +0 -1
  63. package/dist/services/index.d.ts +1 -0
  64. package/dist/services/index.js +1 -0
  65. package/dist/services/items.js +3 -1
  66. package/dist/services/mail/index.d.ts +2 -1
  67. package/dist/services/mail/index.js +4 -1
  68. package/dist/services/payload.js +15 -14
  69. package/dist/services/tus/data-store.d.ts +0 -1
  70. package/dist/services/users.js +3 -2
  71. package/dist/services/versions.js +59 -44
  72. package/dist/types/graphql.d.ts +0 -1
  73. package/dist/utils/apply-diff.js +5 -6
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/compress.d.ts +0 -1
  76. package/dist/utils/delete-from-require-cache.js +1 -1
  77. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
  78. package/dist/utils/generate-hash.js +2 -2
  79. package/dist/utils/get-address.d.ts +0 -3
  80. package/dist/utils/get-cache-headers.d.ts +0 -1
  81. package/dist/utils/get-cache-key.d.ts +0 -1
  82. package/dist/utils/get-column.d.ts +1 -1
  83. package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
  84. package/dist/utils/get-ip-from-req.d.ts +0 -1
  85. package/dist/utils/get-service.js +3 -1
  86. package/dist/utils/get-snapshot.js +1 -1
  87. package/dist/utils/sanitize-query.js +1 -1
  88. package/dist/utils/sanitize-schema.d.ts +1 -1
  89. package/dist/utils/sanitize-schema.js +2 -0
  90. package/dist/utils/should-skip-cache.d.ts +0 -1
  91. package/dist/websocket/authenticate.js +1 -1
  92. package/dist/websocket/controllers/base.d.ts +1 -10
  93. package/dist/websocket/controllers/base.js +15 -21
  94. package/dist/websocket/controllers/graphql.d.ts +0 -3
  95. package/dist/websocket/controllers/index.d.ts +0 -3
  96. package/dist/websocket/controllers/logs.d.ts +4 -5
  97. package/dist/websocket/controllers/logs.js +7 -3
  98. package/dist/websocket/controllers/rest.d.ts +0 -3
  99. package/dist/websocket/controllers/rest.js +1 -1
  100. package/dist/websocket/types.d.ts +0 -6
  101. package/package.json +70 -71
@@ -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, omit, pick } from 'lodash-es';
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 relatedPrimary = this.schema.collections[relatedCollection].primary;
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 = relatedPrimary in relatedRecord;
355
- let relatedPrimaryKey = relatedRecord[relatedPrimary];
354
+ const hasPrimaryKey = relatedPrimaryKeyField in relatedRecord;
355
+ let relatedPrimaryKey = relatedRecord[relatedPrimaryKeyField];
356
356
  const exists = hasPrimaryKey &&
357
357
  !!(await this.knex
358
- .select(relatedPrimary)
358
+ .select(relatedPrimaryKeyField)
359
359
  .from(relatedCollection)
360
- .where({ [relatedPrimary]: relatedPrimaryKey })
360
+ .where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
361
361
  .first());
362
362
  if (exists) {
363
- const fieldsToUpdate = omit(relatedRecord, relatedPrimary);
364
- if (Object.keys(fieldsToUpdate).length > 0) {
365
- await service.updateOne(relatedPrimaryKey, relatedRecord, {
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 fieldsToUpdate = omit(relatedRecord, relatedPrimaryKeyField);
429
- if (Object.keys(fieldsToUpdate).length > 0) {
430
- await service.updateOne(relatedPrimaryKey, relatedRecord, {
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
- await service.updateOne(item[primaryKeyField], {
614
- ...item,
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),
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { TusDriver } from '@directus/storage';
3
2
  import type { Accountability, File, SchemaOverview } from '@directus/types';
4
3
  import stream from 'node:stream';
@@ -260,6 +260,7 @@ export class UsersService extends ItemsService {
260
260
  }
261
261
  }
262
262
  // Manual constraint, see https://github.com/directus/directus/pull/19912
263
+ await this.knex('directus_comments').update({ user_updated: null }).whereIn('user_updated', keys);
263
264
  await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
264
265
  await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
265
266
  await super.deleteMany(keys, opts);
@@ -373,7 +374,7 @@ export class UsersService extends ItemsService {
373
374
  await this.createOne(partialUser);
374
375
  }
375
376
  // We want to be able to re-send the verification email
376
- else if (user.status !== ('unverified')) {
377
+ else if (user.status !== 'unverified') {
377
378
  // To avoid giving attackers infos about registered emails we dont fail for violated unique constraints
378
379
  await stall(STALL_TIME, timeStart);
379
380
  return;
@@ -415,7 +416,7 @@ export class UsersService extends ItemsService {
415
416
  if (scope !== 'pending-registration')
416
417
  throw new ForbiddenError();
417
418
  const user = await this.getUserByEmail(email);
418
- if (user?.status !== ('unverified')) {
419
+ if (user?.status !== 'unverified') {
419
420
  throw new InvalidPayloadError({ reason: 'Invalid verification code' });
420
421
  }
421
422
  await this.updateOne(user.id, { status: 'active' });
@@ -1,10 +1,9 @@
1
1
  import { Action } from '@directus/constants';
2
- import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
+ import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
3
3
  import Joi from 'joi';
4
4
  import { assign, pick } from 'lodash-es';
5
5
  import objectHash from 'object-hash';
6
6
  import { getCache } from '../cache.js';
7
- import getDatabase from '../database/index.js';
8
7
  import emitter from '../emitter.js';
9
8
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
10
9
  import { shouldClearCache } from '../utils/should-clear-cache.js';
@@ -17,19 +16,36 @@ export class VersionsService extends ItemsService {
17
16
  super('directus_versions', options);
18
17
  }
19
18
  async validateCreateData(data) {
20
- if (!data['key'])
21
- throw new InvalidPayloadError({ reason: `"key" is required` });
19
+ const versionCreateSchema = Joi.object({
20
+ key: Joi.string().required(),
21
+ name: Joi.string().allow(null),
22
+ collection: Joi.string().required(),
23
+ item: Joi.string().required(),
24
+ });
25
+ const { error } = versionCreateSchema.validate(data);
26
+ if (error)
27
+ throw new InvalidPayloadError({ reason: error.message });
22
28
  // Reserves the "main" version key for the version query parameter
23
29
  if (data['key'] === 'main')
24
30
  throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
25
- if (!data['collection']) {
26
- throw new InvalidPayloadError({ reason: `"collection" is required` });
31
+ if (this.accountability) {
32
+ try {
33
+ await validateAccess({
34
+ accountability: this.accountability,
35
+ action: 'read',
36
+ collection: data['collection'],
37
+ primaryKeys: [data['item']],
38
+ }, {
39
+ schema: this.schema,
40
+ knex: this.knex,
41
+ });
42
+ }
43
+ catch {
44
+ throw new ForbiddenError();
45
+ }
27
46
  }
28
- if (!data['item'])
29
- throw new InvalidPayloadError({ reason: `"item" is required` });
30
47
  const { CollectionsService } = await import('./collections.js');
31
48
  const collectionsService = new CollectionsService({
32
- accountability: null,
33
49
  knex: this.knex,
34
50
  schema: this.schema,
35
51
  });
@@ -39,7 +55,11 @@ export class VersionsService extends ItemsService {
39
55
  reason: `Content Versioning is not enabled for collection "${data['collection']}"`,
40
56
  });
41
57
  }
42
- const existingVersions = await super.readByQuery({
58
+ const sudoService = new VersionsService({
59
+ knex: this.knex,
60
+ schema: this.schema,
61
+ });
62
+ const existingVersions = await sudoService.readByQuery({
43
63
  aggregate: { count: ['*'] },
44
64
  filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
45
65
  });
@@ -48,32 +68,8 @@ export class VersionsService extends ItemsService {
48
68
  reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
49
69
  });
50
70
  }
51
- // will throw an error if the accountability does not have permission to read the item
52
- if (this.accountability) {
53
- await validateAccess({
54
- accountability: this.accountability,
55
- action: 'read',
56
- collection: data['collection'],
57
- primaryKeys: [data['item']],
58
- }, {
59
- schema: this.schema,
60
- knex: this.knex,
61
- });
62
- }
63
71
  }
64
72
  async getMainItem(collection, item, query) {
65
- // will throw an error if the accountability does not have permission to read the item
66
- if (this.accountability) {
67
- await validateAccess({
68
- accountability: this.accountability,
69
- action: 'read',
70
- collection,
71
- primaryKeys: [item],
72
- }, {
73
- schema: this.schema,
74
- knex: this.knex,
75
- });
76
- }
77
73
  const itemsService = new ItemsService(collection, {
78
74
  knex: this.knex,
79
75
  accountability: this.accountability,
@@ -96,6 +92,7 @@ export class VersionsService extends ItemsService {
96
92
  });
97
93
  return result.map((revision) => revision['delta']);
98
94
  }
95
+ // TODO: Remove legacy need to return a version array in subsequent release
99
96
  async getVersionSaves(key, collection, item) {
100
97
  const filter = {
101
98
  key: { _eq: key },
@@ -107,6 +104,9 @@ export class VersionsService extends ItemsService {
107
104
  const versions = await this.readByQuery({ filter });
108
105
  if (!versions?.[0])
109
106
  return null;
107
+ if (versions[0]['delta']) {
108
+ return [versions[0]['delta']];
109
+ }
110
110
  const saves = await this.getVersionSavesById(versions[0]['id']);
111
111
  return saves;
112
112
  }
@@ -122,7 +122,6 @@ export class VersionsService extends ItemsService {
122
122
  }
123
123
  const keyCombos = new Set();
124
124
  for (const item of data) {
125
- await this.validateCreateData(item);
126
125
  const keyCombo = `${item['key']}-${item['collection']}-${item['item']}`;
127
126
  if (keyCombos.has(keyCombo)) {
128
127
  throw new UnprocessableContentError({
@@ -130,8 +129,6 @@ export class VersionsService extends ItemsService {
130
129
  });
131
130
  }
132
131
  keyCombos.add(keyCombo);
133
- const mainItem = await this.getMainItem(item['collection'], item['item']);
134
- item['hash'] = objectHash(mainItem);
135
132
  }
136
133
  return super.createMany(data, opts);
137
134
  }
@@ -139,7 +136,7 @@ export class VersionsService extends ItemsService {
139
136
  // Only allow updates on "key" and "name" fields
140
137
  const versionUpdateSchema = Joi.object({
141
138
  key: Joi.string(),
142
- name: Joi.string().allow(null).optional(),
139
+ name: Joi.string().allow(null),
143
140
  });
144
141
  const { error } = versionUpdateSchema.validate(data);
145
142
  if (error)
@@ -205,14 +202,25 @@ export class VersionsService extends ItemsService {
205
202
  data: revisionDelta,
206
203
  delta: revisionDelta,
207
204
  });
205
+ let existingDelta = version['delta'];
206
+ if (!existingDelta) {
207
+ const saves = await this.getVersionSavesById(key);
208
+ existingDelta = assign({}, ...saves);
209
+ }
210
+ const finalVersionDelta = assign({}, existingDelta, revisionDelta ? JSON.parse(revisionDelta) : null);
211
+ const sudoService = new ItemsService(this.collection, {
212
+ knex: this.knex,
213
+ schema: this.schema,
214
+ });
215
+ await sudoService.updateOne(key, { delta: finalVersionDelta });
208
216
  const { cache } = getCache();
209
217
  if (shouldClearCache(cache, undefined, collection)) {
210
218
  cache.clear();
211
219
  }
212
- return data;
220
+ return finalVersionDelta;
213
221
  }
214
222
  async promote(version, mainHash, fields) {
215
- const { id, collection, item } = (await this.readOne(version));
223
+ const { id, collection, item, delta } = (await this.readOne(version));
216
224
  // will throw an error if the accountability does not have permission to update the item
217
225
  if (this.accountability) {
218
226
  await validateAccess({
@@ -231,11 +239,18 @@ export class VersionsService extends ItemsService {
231
239
  reason: `Main item has changed since this version was last updated`,
232
240
  });
233
241
  }
234
- const saves = await this.getVersionSavesById(id);
235
- const versionResult = assign({}, ...saves);
242
+ let versionResult;
243
+ if (delta) {
244
+ versionResult = delta;
245
+ }
246
+ else {
247
+ const saves = await this.getVersionSavesById(id);
248
+ versionResult = assign({}, ...saves);
249
+ }
236
250
  const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
237
251
  const itemsService = new ItemsService(collection, {
238
252
  accountability: this.accountability,
253
+ knex: this.knex,
239
254
  schema: this.schema,
240
255
  });
241
256
  const payloadAfterHooks = await emitter.emitFilter(['items.promote', `${collection}.items.promote`], payloadToUpdate, {
@@ -243,7 +258,7 @@ export class VersionsService extends ItemsService {
243
258
  item,
244
259
  version,
245
260
  }, {
246
- database: getDatabase(),
261
+ database: this.knex,
247
262
  schema: this.schema,
248
263
  accountability: this.accountability,
249
264
  });
@@ -254,7 +269,7 @@ export class VersionsService extends ItemsService {
254
269
  item: updatedItemKey,
255
270
  version,
256
271
  }, {
257
- database: getDatabase(),
272
+ database: this.knex,
258
273
  schema: this.schema,
259
274
  accountability: this.accountability,
260
275
  });
@@ -1,4 +1,3 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request, Response } from 'express';
3
2
  import type { DocumentNode } from 'graphql';
4
3
  export interface GraphQLParams {
@@ -26,7 +26,6 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
26
26
  await transaction(database, async (trx) => {
27
27
  const collectionsService = new CollectionsService({ knex: trx, schema });
28
28
  const getNestedCollectionsToCreate = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => diff[0].rhs?.meta?.group === currentLevelCollection);
29
- const getNestedCollectionsToDelete = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => diff[0].lhs?.meta?.group === currentLevelCollection);
30
29
  const createCollections = async (collections) => {
31
30
  for (const { collection, diff } of collections) {
32
31
  if (diff?.[0]?.kind === DiffKind.NEW && diff[0].rhs) {
@@ -82,7 +81,6 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
82
81
  // clean up deleted relations from existing schema
83
82
  schema.relations = schema.relations.filter((r) => r.related_collection !== collection && r.collection !== collection);
84
83
  }
85
- await deleteCollections(getNestedCollectionsToDelete(collection));
86
84
  try {
87
85
  await collectionsService.deleteOne(collection, mutationOptions);
88
86
  }
@@ -122,13 +120,14 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
122
120
  // Create top level collections (no group, or highest level in existing group) first,
123
121
  // then continue with nested collections recursively
124
122
  await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
125
- // delete top level collections (no group) first, then continue with nested collections recursively
126
- await deleteCollections(snapshotDiff.collections.filter(({ diff }) => {
123
+ const collectionsToDelete = snapshotDiff.collections.filter(({ diff }) => {
127
124
  if (diff.length === 0 || diff[0] === undefined)
128
125
  return false;
129
126
  const collectionDiff = diff[0];
130
- return collectionDiff.kind === DiffKind.DELETE && collectionDiff.lhs?.meta?.group === null;
131
- }));
127
+ return collectionDiff.kind === DiffKind.DELETE;
128
+ });
129
+ if (collectionsToDelete.length > 0)
130
+ await deleteCollections(collectionsToDelete);
132
131
  for (const { collection, diff } of snapshotDiff.collections) {
133
132
  if (diff?.[0]?.kind === DiffKind.EDIT || diff?.[0]?.kind === DiffKind.ARRAY) {
134
133
  const currentCollection = currentSnapshot.collections.find((field) => {
@@ -1,7 +1,7 @@
1
1
  import type { Aggregate, Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { AliasMap } from './get-column-path.js';
4
- export declare const generateAlias: (size?: number | undefined) => string;
4
+ export declare const generateAlias: (size?: number) => string;
5
5
  type ApplyQueryOptions = {
6
6
  aliasMap?: AliasMap;
7
7
  isInnerQuery?: boolean;
@@ -1,3 +1,2 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  export declare function compress(raw: Record<string, any> | Record<string, any>[]): Promise<Buffer>;
3
2
  export declare function decompress(compressed: Buffer): Promise<any>;
@@ -7,7 +7,7 @@ export function deleteFromRequireCache(modulePath) {
7
7
  const moduleCachePath = require.resolve(modulePath);
8
8
  delete require.cache[moduleCachePath];
9
9
  }
10
- catch (error) {
10
+ catch {
11
11
  logger.trace(`Module cache not found for ${modulePath}, skipped cache delete.`);
12
12
  }
13
13
  }
@@ -1,6 +1,5 @@
1
1
  import { type FetchAccessLookupOptions } from './fetch-access-lookup.js';
2
- export interface FetchUserCountOptions extends FetchAccessLookupOptions {
3
- }
2
+ export type FetchUserCountOptions = FetchAccessLookupOptions;
4
3
  export interface UserCount {
5
4
  admin: number;
6
5
  app: number;
@@ -3,7 +3,7 @@ import { getConfigFromEnv } from './get-config-from-env.js';
3
3
  export function generateHash(stringToHash) {
4
4
  const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
5
5
  // associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata
6
- 'associatedData' in argon2HashConfigOptions &&
7
- (argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']));
6
+ if ('associatedData' in argon2HashConfigOptions)
7
+ argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']);
8
8
  return argon2.hash(stringToHash, argon2HashConfigOptions);
9
9
  }
@@ -1,5 +1,2 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node/http.js" />
3
- /// <reference types="pino-http" />
4
1
  import * as http from 'http';
5
2
  export declare function getAddress(server: http.Server): {};
@@ -1,4 +1,3 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request } from 'express';
3
2
  /**
4
3
  * Returns the Cache-Control header for the current request
@@ -1,3 +1,2 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request } from 'express';
3
2
  export declare function getCacheKey(req: Request): Promise<string>;
@@ -21,5 +21,5 @@ type GetColumnOptions = OriginalCollectionName | (FunctionColumnOptions & Origin
21
21
  * @param options Optional parameters
22
22
  * @returns Knex raw instance
23
23
  */
24
- export declare function getColumn(knex: Knex, table: string, column: string, alias: string | false | undefined, schema: SchemaOverview, options?: GetColumnOptions): Knex.Raw;
24
+ export declare function getColumn(knex: Knex, table: string, column: string, alias: (string | false) | undefined, schema: SchemaOverview, options?: GetColumnOptions): Knex.Raw;
25
25
  export {};
@@ -1,3 +1,2 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request } from 'express';
3
2
  export declare function getGraphqlQueryAndVariables(req: Request): Pick<any, "query" | "variables">;
@@ -1,3 +1,2 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request } from 'express';
3
2
  export declare function getIPFromReq(req: Request): string | null;
@@ -1,5 +1,5 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
- import { AccessService, ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
2
+ import { AccessService, ActivityService, CommentsService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
3
3
  /**
4
4
  * Select the correct service for the given collection. This allows the individual services to run
5
5
  * their custom checks (f.e. it allows `UsersService` to prevent updating TFA secret from outside).
@@ -10,6 +10,8 @@ export function getService(collection, opts) {
10
10
  return new AccessService(opts);
11
11
  case 'directus_activity':
12
12
  return new ActivityService(opts);
13
+ case 'directus_comments':
14
+ return new CommentsService({ ...opts, serviceOrigin: 'comments' });
13
15
  case 'directus_dashboards':
14
16
  return new DashboardsService(opts);
15
17
  case 'directus_files':
@@ -49,7 +49,7 @@ function sortDeep(raw) {
49
49
  return fromPairs(sorted);
50
50
  }
51
51
  if (isArray(raw)) {
52
- return sortBy(raw);
52
+ return raw.map((raw) => sortDeep(raw));
53
53
  }
54
54
  return raw;
55
55
  }
@@ -192,7 +192,7 @@ function sanitizeAlias(rawAlias) {
192
192
  try {
193
193
  alias = parseJSON(rawAlias);
194
194
  }
195
- catch (err) {
195
+ catch {
196
196
  logger.warn('Invalid value passed for alias query parameter.');
197
197
  }
198
198
  }
@@ -16,7 +16,7 @@ export declare function sanitizeCollection(collection: Collection | undefined):
16
16
  * @returns sanitized field
17
17
  */
18
18
  export declare function sanitizeField(field: Field | undefined, sanitizeAllSchema?: boolean): Partial<Field> | undefined;
19
- export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "data_type" | "default_value" | "max_length" | "numeric_scale" | "is_nullable" | "is_unique" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
19
+ export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "data_type" | "default_value" | "max_length" | "numeric_scale" | "is_nullable" | "is_unique" | "is_indexed" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
20
20
  /**
21
21
  * Pick certain database vendor specific relation properties that should be compared when performing diff
22
22
  *
@@ -34,6 +34,7 @@ export function sanitizeField(field, sanitizeAllSchema = false) {
34
34
  'schema.numeric_scale',
35
35
  'schema.is_nullable',
36
36
  'schema.is_unique',
37
+ 'schema.is_indexed',
37
38
  'schema.is_primary_key',
38
39
  'schema.is_generated',
39
40
  'schema.generation_expression',
@@ -54,6 +55,7 @@ export function sanitizeColumn(column) {
54
55
  'numeric_scale',
55
56
  'is_nullable',
56
57
  'is_unique',
58
+ 'is_indexed',
57
59
  'is_primary_key',
58
60
  'is_generated',
59
61
  'generation_expression',
@@ -1,4 +1,3 @@
1
- /// <reference types="cookie-parser" />
2
1
  import type { Request } from 'express';
3
2
  /**
4
3
  * Whether to skip caching for the current request
@@ -28,7 +28,7 @@ export async function authenticateConnection(message) {
28
28
  const expires_at = getExpiresAtForToken(access_token);
29
29
  return { accountability, expires_at, refresh_token };
30
30
  }
31
- catch (error) {
31
+ catch {
32
32
  throw new WebSocketError('auth', 'AUTH_FAILED', 'Authentication failed.', message['uid']);
33
33
  }
34
34
  }
@@ -1,8 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="node" resolution-mode="require"/>
4
- /// <reference types="node/http.js" />
5
- /// <reference types="pino-http" />
6
1
  import type { Accountability } from '@directus/types';
7
2
  import type { IncomingMessage, Server as httpServer } from 'http';
8
3
  import type { RateLimiterAbstract } from 'rate-limiter-flexible';
@@ -32,12 +27,8 @@ export default abstract class SocketController {
32
27
  createClient(ws: WebSocket, { accountability, expires_at }: AuthenticationState): WebSocketClient;
33
28
  protected parseMessage(data: string): WebSocketMessage;
34
29
  protected handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage): Promise<void>;
30
+ protected checkUserRequirements(_accountability: Accountability | null): void;
35
31
  setTokenExpireTimer(client: WebSocketClient): void;
36
32
  checkClientTokens(): void;
37
- meetsAdminRequirement({ socket, client, accountability, }: {
38
- socket?: UpgradeContext['socket'];
39
- client?: WebSocketClient | WebSocket;
40
- accountability: Accountability | null;
41
- }): boolean;
42
33
  terminate(): void;
43
34
  }
@@ -60,7 +60,6 @@ export default class SocketController {
60
60
  authentication: {
61
61
  mode: authMode.data,
62
62
  timeout: authTimeout,
63
- requireAdmin: false,
64
63
  },
65
64
  };
66
65
  }
@@ -140,8 +139,15 @@ export default class SocketController {
140
139
  socket.destroy();
141
140
  return;
142
141
  }
143
- if (!this.meetsAdminRequirement({ socket, accountability }))
142
+ try {
143
+ this.checkUserRequirements(accountability);
144
+ }
145
+ catch {
146
+ logger.debug('WebSocket upgrade denied - ' + JSON.stringify(accountability || 'invalid'));
147
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
148
+ socket.destroy();
144
149
  return;
150
+ }
145
151
  this.server.handleUpgrade(request, socket, head, async (ws) => {
146
152
  this.catchInvalidMessages(ws);
147
153
  const state = { accountability, expires_at };
@@ -156,8 +162,7 @@ export default class SocketController {
156
162
  if (getMessageType(payload) !== 'auth')
157
163
  throw new Error();
158
164
  const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
159
- if (this.meetsAdminRequirement({ client: ws, accountability: state.accountability }))
160
- return;
165
+ this.checkUserRequirements(state.accountability);
161
166
  ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
162
167
  this.server.emit('connection', ws, state);
163
168
  }
@@ -238,7 +243,7 @@ export default class SocketController {
238
243
  try {
239
244
  message = WebSocketMessage.parse(parseJSON(data));
240
245
  }
241
- catch (err) {
246
+ catch {
242
247
  throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
243
248
  }
244
249
  return message;
@@ -246,8 +251,7 @@ export default class SocketController {
246
251
  async handleAuthRequest(client, message) {
247
252
  try {
248
253
  const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
249
- if (!this.meetsAdminRequirement({ client, accountability }))
250
- return;
254
+ this.checkUserRequirements(accountability);
251
255
  client.accountability = accountability;
252
256
  client.expires_at = expires_at;
253
257
  this.setTokenExpireTimer(client);
@@ -269,6 +273,10 @@ export default class SocketController {
269
273
  }
270
274
  }
271
275
  }
276
+ checkUserRequirements(_accountability) {
277
+ // there are no requirements in the abstract class
278
+ return;
279
+ }
272
280
  setTokenExpireTimer(client) {
273
281
  if (client.auth_timer !== null) {
274
282
  // clear up old timeouts if needed
@@ -305,20 +313,6 @@ export default class SocketController {
305
313
  }
306
314
  }, TOKEN_CHECK_INTERVAL);
307
315
  }
308
- meetsAdminRequirement({ socket, client, accountability, }) {
309
- if (!this.authentication.requireAdmin || accountability?.admin)
310
- return true;
311
- logger.debug('WebSocket connection denied - ' + JSON.stringify(accountability || 'invalid'));
312
- if (socket) {
313
- socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
314
- socket.destroy();
315
- }
316
- else if (client) {
317
- handleWebSocketError(client, new WebSocketError('auth', 'UNAUTHORIZED', 'Unauthorized.'), 'auth');
318
- client.close();
319
- }
320
- return false;
321
- }
322
316
  terminate() {
323
317
  if (this.authInterval)
324
318
  clearInterval(this.authInterval);
@@ -1,6 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node/http.js" />
3
- /// <reference types="pino-http" />
4
1
  import type { Server } from 'graphql-ws';
5
2
  import type { Server as httpServer } from 'http';
6
3
  import type { GraphQLSocket, UpgradeContext, WebSocketClient } from '../types.js';
@@ -1,6 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node/http.js" />
3
- /// <reference types="pino-http" />
4
1
  import type { Server as httpServer } from 'http';
5
2
  import { GraphQLSubscriptionController } from './graphql.js';
6
3
  import { LogsController } from './logs.js';