@directus/api 22.1.0 → 22.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 (120) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/cache.d.ts +2 -2
  3. package/dist/cache.js +2 -2
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.js +1 -0
  6. package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
  7. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
  8. package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
  9. package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
  10. package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
  11. package/dist/database/helpers/fn/types.d.ts +6 -3
  12. package/dist/database/helpers/fn/types.js +2 -2
  13. package/dist/database/helpers/index.d.ts +2 -0
  14. package/dist/database/helpers/index.js +2 -0
  15. package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
  16. package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
  17. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
  18. package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
  19. package/dist/database/helpers/nullable-update/index.d.ts +7 -0
  20. package/dist/database/helpers/nullable-update/index.js +7 -0
  21. package/dist/database/helpers/nullable-update/types.d.ts +7 -0
  22. package/dist/database/helpers/nullable-update/types.js +12 -0
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +20 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +33 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +21 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +23 -0
  33. package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
  34. package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
  35. package/dist/database/helpers/schema/types.d.ts +5 -0
  36. package/dist/database/helpers/schema/types.js +3 -0
  37. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
  38. package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
  39. package/dist/database/index.d.ts +1 -1
  40. package/dist/database/index.js +2 -2
  41. package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
  42. package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
  43. package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
  44. package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
  45. package/dist/database/run-ast/lib/get-db-query.js +23 -13
  46. package/dist/database/run-ast/run-ast.d.ts +2 -2
  47. package/dist/database/run-ast/run-ast.js +14 -7
  48. package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
  49. package/dist/database/run-ast/utils/apply-case-when.js +2 -2
  50. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
  51. package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
  52. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
  53. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
  54. package/dist/extensions/manager.js +2 -2
  55. package/dist/logger/index.d.ts +6 -0
  56. package/dist/logger/index.js +79 -28
  57. package/dist/logger/logs-stream.d.ts +11 -0
  58. package/dist/logger/logs-stream.js +41 -0
  59. package/dist/middleware/respond.js +1 -0
  60. package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
  61. package/dist/permissions/lib/fetch-permissions.js +5 -39
  62. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
  63. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
  64. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
  65. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
  66. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
  67. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
  68. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
  69. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
  70. package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
  71. package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
  72. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
  73. package/dist/permissions/modules/process-payload/process-payload.js +4 -5
  74. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
  75. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
  76. package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
  77. package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
  78. package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
  79. package/dist/request/is-denied-ip.js +7 -1
  80. package/dist/server.js +4 -2
  81. package/dist/services/fields.d.ts +1 -1
  82. package/dist/services/fields.js +66 -25
  83. package/dist/services/import-export.js +2 -2
  84. package/dist/services/items.js +1 -1
  85. package/dist/services/mail/index.js +1 -5
  86. package/dist/services/meta.js +8 -7
  87. package/dist/services/notifications.d.ts +0 -4
  88. package/dist/services/notifications.js +8 -6
  89. package/dist/services/permissions.js +19 -19
  90. package/dist/services/server.js +8 -1
  91. package/dist/services/specifications.js +7 -7
  92. package/dist/services/users.js +4 -1
  93. package/dist/utils/apply-query.d.ts +3 -3
  94. package/dist/utils/apply-query.js +25 -20
  95. package/dist/utils/get-address.d.ts +1 -1
  96. package/dist/utils/get-address.js +6 -1
  97. package/dist/utils/get-allowed-log-levels.d.ts +3 -0
  98. package/dist/utils/get-allowed-log-levels.js +11 -0
  99. package/dist/utils/get-column.d.ts +8 -4
  100. package/dist/utils/get-column.js +10 -2
  101. package/dist/utils/get-schema.js +19 -24
  102. package/dist/utils/parse-filter-key.js +1 -5
  103. package/dist/utils/sanitize-query.js +1 -1
  104. package/dist/utils/sanitize-schema.d.ts +1 -1
  105. package/dist/websocket/controllers/base.d.ts +10 -10
  106. package/dist/websocket/controllers/base.js +22 -3
  107. package/dist/websocket/controllers/graphql.js +3 -1
  108. package/dist/websocket/controllers/index.d.ts +4 -0
  109. package/dist/websocket/controllers/index.js +12 -0
  110. package/dist/websocket/controllers/logs.d.ts +18 -0
  111. package/dist/websocket/controllers/logs.js +50 -0
  112. package/dist/websocket/controllers/rest.js +3 -1
  113. package/dist/websocket/handlers/index.d.ts +1 -0
  114. package/dist/websocket/handlers/index.js +21 -3
  115. package/dist/websocket/handlers/logs.d.ts +31 -0
  116. package/dist/websocket/handlers/logs.js +121 -0
  117. package/dist/websocket/messages.d.ts +26 -0
  118. package/dist/websocket/messages.js +9 -0
  119. package/dist/websocket/types.d.ts +7 -0
  120. package/package.json +27 -26
@@ -5,7 +5,7 @@ import { createInspector } from '@directus/schema';
5
5
  import { addFieldFlag, toArray } from '@directus/utils';
6
6
  import { isEqual, isNil, merge } from 'lodash-es';
7
7
  import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
8
- import { ALIAS_TYPES } from '../constants.js';
8
+ import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js';
9
9
  import { translateDatabaseError } from '../database/errors/translate.js';
10
10
  import { getHelpers } from '../database/helpers/index.js';
11
11
  import getDatabase, { getSchemaInspector } from '../database/index.js';
@@ -384,8 +384,16 @@ export class FieldsService {
384
384
  }
385
385
  if (hookAdjustedField.schema) {
386
386
  const existingColumn = await this.columnInfo(collection, hookAdjustedField.field);
387
- if (hookAdjustedField.schema?.is_nullable === true && existingColumn.is_primary_key) {
388
- throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
387
+ if (existingColumn.is_primary_key) {
388
+ if (hookAdjustedField.schema?.is_nullable === true) {
389
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
390
+ }
391
+ if (hookAdjustedField.schema?.is_unique === false) {
392
+ throw new InvalidPayloadError({ reason: 'Primary key must be unique' });
393
+ }
394
+ if (hookAdjustedField.schema?.is_indexed === true) {
395
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be indexed' });
396
+ }
389
397
  }
390
398
  // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
391
399
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
@@ -621,7 +629,7 @@ export class FieldsService {
621
629
  }
622
630
  }
623
631
  }
624
- addColumnToTable(table, field, alter = null) {
632
+ addColumnToTable(table, field, existing = null) {
625
633
  let column;
626
634
  // Don't attempt to add a DB column for alias / corrupt fields
627
635
  if (field.type === 'alias' || field.type === 'unknown')
@@ -662,45 +670,78 @@ export class FieldsService {
662
670
  else {
663
671
  throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
664
672
  }
665
- if (field.schema?.default_value !== undefined) {
666
- if (typeof field.schema.default_value === 'string' &&
667
- (field.schema.default_value.toLowerCase() === 'now()' || field.schema.default_value === 'CURRENT_TIMESTAMP')) {
673
+ const setDefaultValue = (defaultValue) => {
674
+ const newDefaultValueIsString = typeof defaultValue === 'string';
675
+ const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
676
+ const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
677
+ const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp;
678
+ const newDefaultIsAFunction = newDefaultValueIsString && ALLOWED_DB_DEFAULT_FUNCTIONS.includes(defaultValue);
679
+ const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')');
680
+ if (newDefaultIsSetToCurrentTime) {
668
681
  column.defaultTo(this.knex.fn.now());
669
682
  }
670
- else if (typeof field.schema.default_value === 'string' &&
671
- field.schema.default_value.includes('CURRENT_TIMESTAMP(') &&
672
- field.schema.default_value.includes(')')) {
673
- const precision = field.schema.default_value.match(REGEX_BETWEEN_PARENS)[1];
683
+ else if (newDefaultIsTimestampWithPrecision) {
684
+ const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1];
674
685
  column.defaultTo(this.knex.fn.now(Number(precision)));
675
686
  }
687
+ else if (newDefaultIsAFunction) {
688
+ column.defaultTo(this.knex.raw(defaultValue));
689
+ }
676
690
  else {
677
- column.defaultTo(field.schema.default_value);
691
+ column.defaultTo(defaultValue);
678
692
  }
679
- }
680
- if (field.schema?.is_nullable === false) {
681
- if (!alter || alter.is_nullable === true) {
693
+ };
694
+ // for a new item, set the default value and nullable as provided without any further considerations
695
+ if (!existing) {
696
+ if (field.schema?.default_value !== undefined) {
697
+ setDefaultValue(field.schema.default_value);
698
+ }
699
+ if (field.schema?.is_nullable || field.schema?.is_nullable === undefined) {
700
+ column.nullable();
701
+ }
702
+ else {
682
703
  column.notNullable();
683
704
  }
684
705
  }
685
706
  else {
686
- if (!alter || alter.is_nullable === false) {
687
- column.nullable();
707
+ // for an existing item: if nullable option changed, we have to provide the default values as well and actually vice versa
708
+ // see https://knexjs.org/guide/schema-builder.html#alter
709
+ // To overwrite a nullable option with the same value this is not possible for Oracle though, hence the DB helper
710
+ if (field.schema?.default_value !== undefined || field.schema?.is_nullable !== undefined) {
711
+ this.helpers.nullableUpdate.updateNullableValue(column, field, existing);
712
+ let defaultValue = null;
713
+ if (field.schema?.default_value !== undefined) {
714
+ defaultValue = field.schema.default_value;
715
+ }
716
+ else if (existing.default_value !== undefined) {
717
+ defaultValue = existing.default_value;
718
+ }
719
+ setDefaultValue(defaultValue);
688
720
  }
689
721
  }
690
722
  if (field.schema?.is_primary_key) {
691
723
  column.primary().notNullable();
692
724
  }
693
- else if (field.schema?.is_unique === true) {
694
- if (!alter || alter.is_unique === false) {
695
- column.unique();
725
+ else if (!existing?.is_primary_key) {
726
+ // primary key will already have unique/index constraints
727
+ if (field.schema?.is_unique === true) {
728
+ if (!existing || existing.is_unique === false) {
729
+ column.unique();
730
+ }
696
731
  }
697
- }
698
- else if (field.schema?.is_unique === false) {
699
- if (alter && alter.is_unique === true) {
700
- table.dropUnique([field.field]);
732
+ else if (field.schema?.is_unique === false) {
733
+ if (existing && existing.is_unique === true) {
734
+ table.dropUnique([field.field]);
735
+ }
736
+ }
737
+ if (field.schema?.is_indexed === true && !existing?.is_indexed) {
738
+ column.index();
739
+ }
740
+ else if (field.schema?.is_indexed === false && existing?.is_indexed) {
741
+ table.dropIndex([field.field]);
701
742
  }
702
743
  }
703
- if (alter) {
744
+ if (existing) {
704
745
  column.alter();
705
746
  }
706
747
  }
@@ -133,6 +133,8 @@ export class ImportService {
133
133
  };
134
134
  const PapaOptions = {
135
135
  header: true,
136
+ // Trim whitespaces in headers, including the byte order mark (BOM) zero-width no-break space
137
+ transformHeader: (header) => header.trim(),
136
138
  transform,
137
139
  };
138
140
  return new Promise((resolve, reject) => {
@@ -304,7 +306,6 @@ export class ExportService {
304
306
  const savedFile = await filesService.uploadOne(createReadStream(tmpFile.path), fileWithDefaults);
305
307
  if (this.accountability?.user) {
306
308
  const notificationsService = new NotificationsService({
307
- accountability: this.accountability,
308
309
  schema: this.schema,
309
310
  });
310
311
  const usersService = new UsersService({
@@ -333,7 +334,6 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
333
334
  logger.error(err, `Couldn't export ${collection}: ${err.message}`);
334
335
  if (this.accountability?.user) {
335
336
  const notificationsService = new NotificationsService({
336
- accountability: this.accountability,
337
337
  schema: this.schema,
338
338
  });
339
339
  await notificationsService.createOne({
@@ -370,7 +370,7 @@ export class ItemsService {
370
370
  knex: this.knex,
371
371
  });
372
372
  ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
373
- const records = await runAst(ast, this.schema, {
373
+ const records = await runAst(ast, this.schema, this.accountability, {
374
374
  knex: this.knex,
375
375
  // GraphQL requires relational keys to be returned regardless
376
376
  stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
@@ -36,11 +36,7 @@ export class MailService {
36
36
  }
37
37
  }
38
38
  async send(options) {
39
- const payload = await emitter.emitFilter(`email.send`, options, {
40
- database: getDatabase(),
41
- schema: null,
42
- accountability: null,
43
- });
39
+ const payload = await emitter.emitFilter(`email.send`, options, {});
44
40
  if (!payload)
45
41
  return null;
46
42
  const { template, ...emailOptions } = payload;
@@ -45,14 +45,14 @@ export class MetaService {
45
45
  action: 'read',
46
46
  policies,
47
47
  accountability: this.accountability,
48
- ...(collection ? { collections: [collection] } : {}),
49
48
  }, context);
50
- const rules = dedupeAccess(permissions);
49
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
50
+ const rules = dedupeAccess(collectionPermissions);
51
51
  const cases = rules.map(({ rule }) => rule);
52
52
  const filter = {
53
53
  _or: cases,
54
54
  };
55
- const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases);
55
+ const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions);
56
56
  hasJoins = result.hasJoins;
57
57
  }
58
58
  if (hasJoins) {
@@ -70,6 +70,7 @@ export class MetaService {
70
70
  let filter = query.filter || {};
71
71
  let hasJoins = false;
72
72
  let cases = [];
73
+ let permissions = [];
73
74
  if (this.accountability && this.accountability.admin === false) {
74
75
  const context = { knex: this.knex, schema: this.schema };
75
76
  await validateAccess({
@@ -78,13 +79,13 @@ export class MetaService {
78
79
  collection,
79
80
  }, context);
80
81
  const policies = await fetchPolicies(this.accountability, context);
81
- const permissions = await fetchPermissions({
82
+ permissions = await fetchPermissions({
82
83
  action: 'read',
83
84
  policies,
84
85
  accountability: this.accountability,
85
- ...(collection ? { collections: [collection] } : {}),
86
86
  }, context);
87
- const rules = dedupeAccess(permissions);
87
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
88
+ const rules = dedupeAccess(collectionPermissions);
88
89
  cases = rules.map(({ rule }) => rule);
89
90
  const permissionsFilter = {
90
91
  _or: cases,
@@ -97,7 +98,7 @@ export class MetaService {
97
98
  }
98
99
  }
99
100
  if (Object.keys(filter).length > 0) {
100
- ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases));
101
+ ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions));
101
102
  }
102
103
  if (query.search) {
103
104
  applySearch(this.knex, this.schema, dbQuery, query.search, collection);
@@ -1,11 +1,7 @@
1
1
  import type { Notification, PrimaryKey } from '@directus/types';
2
2
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
3
  import { ItemsService } from './items.js';
4
- import { MailService } from './mail/index.js';
5
- import { UsersService } from './users.js';
6
4
  export declare class NotificationsService extends ItemsService {
7
- usersService: UsersService;
8
- mailService: MailService;
9
5
  constructor(options: AbstractServiceOptions);
10
6
  createOne(data: Partial<Notification>, opts?: MutationOptions): Promise<PrimaryKey>;
11
7
  sendEmail(data: Partial<Notification>): Promise<void>;
@@ -10,12 +10,8 @@ import { UsersService } from './users.js';
10
10
  const env = useEnv();
11
11
  const logger = useLogger();
12
12
  export class NotificationsService extends ItemsService {
13
- usersService;
14
- mailService;
15
13
  constructor(options) {
16
14
  super('directus_notifications', options);
17
- this.usersService = new UsersService({ schema: this.schema });
18
- this.mailService = new MailService({ schema: this.schema, accountability: this.accountability });
19
15
  }
20
16
  async createOne(data, opts) {
21
17
  const response = await super.createOne(data, opts);
@@ -24,7 +20,8 @@ export class NotificationsService extends ItemsService {
24
20
  }
25
21
  async sendEmail(data) {
26
22
  if (data.recipient) {
27
- const user = await this.usersService.readOne(data.recipient, {
23
+ const usersService = new UsersService({ schema: this.schema, knex: this.knex });
24
+ const user = await usersService.readOne(data.recipient, {
28
25
  fields: ['id', 'email', 'email_notifications', 'role'],
29
26
  });
30
27
  if (user['email'] && user['email_notifications'] === true) {
@@ -38,7 +35,12 @@ export class NotificationsService extends ItemsService {
38
35
  roles,
39
36
  ip: null,
40
37
  }, this.knex);
41
- this.mailService
38
+ const mailService = new MailService({
39
+ schema: this.schema,
40
+ knex: this.knex,
41
+ accountability: this.accountability,
42
+ });
43
+ mailService
42
44
  .send({
43
45
  template: {
44
46
  name: 'base',
@@ -1,5 +1,8 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
+ import { uniq } from 'lodash-es';
2
3
  import { clearSystemCache } from '../cache.js';
4
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
5
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
3
6
  import { withAppMinimalPermissions } from '../permissions/lib/with-app-minimal-permissions.js';
4
7
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
5
8
  import { ItemsService } from './items.js';
@@ -105,27 +108,24 @@ export class PermissionsService extends ItemsService {
105
108
  .catch(() => { });
106
109
  }));
107
110
  if (schema?.singleton && itemPermissions.update.access) {
108
- const query = {
109
- filter: {
110
- _and: [
111
- ...(this.accountability?.role ? [{ role: { _eq: this.accountability.role } }] : []),
112
- { collection: { _eq: collection } },
113
- { action: { _eq: updateAction } },
114
- ],
115
- },
116
- fields: ['presets', 'fields'],
117
- };
118
- try {
119
- const result = await this.readByQuery(query);
120
- const permission = result[0];
121
- if (permission) {
122
- itemPermissions.update.presets = permission['presets'];
123
- itemPermissions.update.fields = permission['fields'];
111
+ const context = { schema: this.schema, knex: this.knex };
112
+ const policies = await fetchPolicies(this.accountability, context);
113
+ const permissions = await fetchPermissions({ policies, accountability: this.accountability, action: updateAction, collections: [collection] }, context);
114
+ let fields = [];
115
+ let presets = {};
116
+ for (const permission of permissions) {
117
+ if (permission.fields && fields[0] !== '*') {
118
+ fields = uniq([...fields, ...permission.fields]);
119
+ if (fields.includes('*')) {
120
+ fields = ['*'];
121
+ }
122
+ }
123
+ if (permission.presets) {
124
+ presets = { ...(presets ?? {}), ...permission.presets };
124
125
  }
125
126
  }
126
- catch {
127
- // No permission
128
- }
127
+ itemPermissions.update.fields = fields;
128
+ itemPermissions.update.presets = presets;
129
129
  }
130
130
  return itemPermissions;
131
131
  }
@@ -5,6 +5,7 @@ import { merge } from 'lodash-es';
5
5
  import { Readable } from 'node:stream';
6
6
  import { performance } from 'perf_hooks';
7
7
  import { getCache } from '../cache.js';
8
+ import { RESUMABLE_UPLOADS } from '../constants.js';
8
9
  import getDatabase, { hasDatabaseConnection } from '../database/index.js';
9
10
  import { useLogger } from '../logger/index.js';
10
11
  import getMailer from '../mailer.js';
@@ -12,8 +13,8 @@ import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
12
13
  import { rateLimiter } from '../middleware/rate-limiter-ip.js';
13
14
  import { SERVER_ONLINE } from '../server.js';
14
15
  import { getStorage } from '../storage/index.js';
16
+ import { getAllowedLogLevels } from '../utils/get-allowed-log-levels.js';
15
17
  import { SettingsService } from './settings.js';
16
- import { RESUMABLE_UPLOADS } from '../constants.js';
17
18
  const env = useEnv();
18
19
  const logger = useLogger();
19
20
  export class ServerService {
@@ -95,6 +96,12 @@ export class ServerService {
95
96
  info['websocket'].heartbeat = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED'])
96
97
  ? env['WEBSOCKETS_HEARTBEAT_PERIOD']
97
98
  : false;
99
+ info['websocket'].logs =
100
+ toBoolean(env['WEBSOCKETS_LOGS_ENABLED']) && this.accountability.admin
101
+ ? {
102
+ allowedLogLevels: getAllowedLogLevels(env['WEBSOCKETS_LOGS_LEVEL'] || 'info'),
103
+ }
104
+ : false;
98
105
  }
99
106
  else {
100
107
  info['websocket'] = false;
@@ -37,20 +37,20 @@ class OASSpecsService {
37
37
  this.schema = options.schema;
38
38
  }
39
39
  async generate(host) {
40
- let schema = this.schema;
40
+ let schemaForSpec = this.schema;
41
41
  let permissions = [];
42
42
  if (this.accountability && this.accountability.admin !== true) {
43
43
  const allowedFields = await fetchAllowedFieldMap({
44
44
  accountability: this.accountability,
45
45
  action: 'read',
46
- }, { schema, knex: this.knex });
47
- schema = reduceSchema(schema, allowedFields);
48
- const policies = await fetchPolicies(this.accountability, { schema, knex: this.knex });
49
- permissions = await fetchPermissions({ action: 'read', policies, accountability: this.accountability }, { schema, knex: this.knex });
46
+ }, { schema: this.schema, knex: this.knex });
47
+ schemaForSpec = reduceSchema(this.schema, allowedFields);
48
+ const policies = await fetchPolicies(this.accountability, { schema: this.schema, knex: this.knex });
49
+ permissions = await fetchPermissions({ policies, accountability: this.accountability }, { schema: this.schema, knex: this.knex });
50
50
  }
51
- const tags = await this.generateTags(schema);
51
+ const tags = await this.generateTags(schemaForSpec);
52
52
  const paths = await this.generatePaths(permissions, tags);
53
- const components = await this.generateComponents(schema, tags);
53
+ const components = await this.generateComponents(schemaForSpec, tags);
54
54
  const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
55
55
  const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
56
56
  const spec = {
@@ -102,7 +102,10 @@ export class UsersService extends ItemsService {
102
102
  */
103
103
  inviteUrl(email, url) {
104
104
  const payload = { email, scope: 'invite' };
105
- const token = jwt.sign(payload, getSecret(), { expiresIn: '7d', issuer: 'directus' });
105
+ const token = jwt.sign(payload, getSecret(), {
106
+ expiresIn: env['USER_INVITE_TOKEN_TTL'],
107
+ issuer: 'directus',
108
+ });
106
109
  return (url ? new Url(url) : new Url(env['PUBLIC_URL']).addPath('admin', 'accept-invite'))
107
110
  .setQuery('token', token)
108
111
  .toString();
@@ -1,4 +1,4 @@
1
- import type { Aggregate, Filter, Query, SchemaOverview } from '@directus/types';
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
4
  export declare const generateAlias: (size?: number | undefined) => string;
@@ -11,7 +11,7 @@ type ApplyQueryOptions = {
11
11
  /**
12
12
  * Apply the Query to a given Knex query builder instance
13
13
  */
14
- export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], options?: ApplyQueryOptions): {
14
+ export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], permissions: Permission[], options?: ApplyQueryOptions): {
15
15
  query: Knex.QueryBuilder<any, any>;
16
16
  hasJoins: boolean;
17
17
  hasMultiRelationalFilter: boolean;
@@ -34,7 +34,7 @@ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery:
34
34
  };
35
35
  export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
36
36
  export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
37
- export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[]): {
37
+ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[], permissions: Permission[]): {
38
38
  query: Knex.QueryBuilder<any, any>;
39
39
  hasJoins: boolean;
40
40
  hasMultiRelationalFilter: boolean;
@@ -5,6 +5,7 @@ import { clone, isPlainObject } from 'lodash-es';
5
5
  import { customAlphabet } from 'nanoid/non-secure';
6
6
  import { getHelpers } from '../database/helpers/index.js';
7
7
  import { applyCaseWhen } from '../database/run-ast/utils/apply-case-when.js';
8
+ import { getCases } from '../permissions/modules/process-ast/lib/get-cases.js';
8
9
  import { getColumnPath } from './get-column-path.js';
9
10
  import { getColumn } from './get-column.js';
10
11
  import { getRelationInfo } from './get-relation-info.js';
@@ -15,7 +16,7 @@ export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
15
16
  /**
16
17
  * Apply the Query to a given Knex query builder instance
17
18
  */
18
- export default function applyQuery(knex, collection, dbQuery, query, schema, cases, options) {
19
+ export default function applyQuery(knex, collection, dbQuery, query, schema, cases, permissions, options) {
19
20
  const aliasMap = options?.aliasMap ?? Object.create(null);
20
21
  let hasJoins = false;
21
22
  let hasMultiRelationalFilter = false;
@@ -42,7 +43,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
42
43
  // you're actually allowed to read
43
44
  const filter = joinFilterWithCases(query.filter, cases);
44
45
  if (filter) {
45
- const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases);
46
+ const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases, permissions);
46
47
  if (!hasJoins) {
47
48
  hasJoins = filterResult.hasJoins;
48
49
  }
@@ -58,6 +59,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
58
59
  aliasMap,
59
60
  cases,
60
61
  table: collection,
62
+ permissions,
61
63
  }, {
62
64
  knex,
63
65
  schema,
@@ -261,7 +263,7 @@ export function applyOffset(knex, rootQuery, offset) {
261
263
  getHelpers(knex).schema.applyOffset(rootQuery, offset);
262
264
  }
263
265
  }
264
- export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases) {
266
+ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases, permissions) {
265
267
  const helpers = getHelpers(knex);
266
268
  const relations = schema.relations;
267
269
  let hasJoins = false;
@@ -349,24 +351,27 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
349
351
  if (relationType === 'o2a') {
350
352
  pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
351
353
  }
352
- const subQueryBuilder = (filter) => (subQueryKnex) => {
353
- const field = relation.field;
354
- const collection = relation.collection;
355
- const column = `${collection}.${field}`;
356
- subQueryKnex
357
- .select({ [field]: column })
358
- .from(collection)
359
- .whereNotNull(column);
360
- applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases);
361
- };
362
354
  const childKey = Object.keys(value)?.[0];
363
- if (childKey === '_none') {
364
- dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0]));
365
- continue;
366
- }
367
- else if (childKey === '_some') {
368
- dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0]));
369
- continue;
355
+ if (childKey === '_none' || childKey === '_some') {
356
+ const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
357
+ const field = relation.field;
358
+ const collection = relation.collection;
359
+ const column = `${collection}.${field}`;
360
+ subQueryKnex
361
+ .select({ [field]: column })
362
+ .from(collection)
363
+ .whereNotNull(column);
364
+ applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
365
+ };
366
+ const { cases: subCases } = getCases(relation.collection, permissions, []);
367
+ if (childKey === '_none') {
368
+ dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
369
+ continue;
370
+ }
371
+ else if (childKey === '_some') {
372
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
373
+ continue;
374
+ }
370
375
  }
371
376
  }
372
377
  if (filterPath.includes('_none') || filterPath.includes('_some')) {
@@ -2,4 +2,4 @@
2
2
  /// <reference types="node/http.js" />
3
3
  /// <reference types="pino-http" />
4
4
  import * as http from 'http';
5
- export declare function getAddress(server: http.Server): string | undefined;
5
+ export declare function getAddress(server: http.Server): {};
@@ -1,9 +1,14 @@
1
1
  import * as http from 'http';
2
+ import { useEnv } from '@directus/env';
2
3
  export function getAddress(server) {
4
+ const env = useEnv();
3
5
  const address = server.address();
4
6
  if (address === null) {
5
7
  // Before the 'listening' event has been emitted or after calling server.close()
6
- return;
8
+ if (env['UNIX_SOCKET_PATH']) {
9
+ return env['UNIX_SOCKET_PATH'];
10
+ }
11
+ return `${env['HOST']}:${env['PORT']}`;
7
12
  }
8
13
  if (typeof address === 'string') {
9
14
  // unix path
@@ -0,0 +1,3 @@
1
+ export declare const getAllowedLogLevels: (level: string) => {
2
+ [k: string]: number;
3
+ };
@@ -0,0 +1,11 @@
1
+ import { useLogger } from '../logger/index.js';
2
+ const logger = useLogger();
3
+ export const getAllowedLogLevels = (level) => {
4
+ const levelValue = logger.levels.values[level];
5
+ if (levelValue === undefined) {
6
+ throw new Error(`Invalid "${level}" log level`);
7
+ }
8
+ return Object.fromEntries(Object.entries(logger.levels.values)
9
+ .filter(([_, value]) => value >= levelValue)
10
+ .sort((a, b) => a[1] - b[1]));
11
+ };
@@ -1,10 +1,14 @@
1
- import type { Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
- type GetColumnOptions = {
4
- query?: Query | undefined;
5
- cases?: Filter[];
3
+ type FunctionColumnOptions = {
4
+ query: Query;
5
+ cases: Filter[];
6
+ permissions: Permission[];
7
+ };
8
+ type OriginalCollectionName = {
6
9
  originalCollectionName?: string | undefined;
7
10
  };
11
+ type GetColumnOptions = OriginalCollectionName | (FunctionColumnOptions & OriginalCollectionName);
8
12
  /**
9
13
  * Return column prefixed by table. If column includes functions (like `year(date_created)`), the
10
14
  * column is replaced with the appropriate SQL
@@ -29,8 +29,13 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
29
29
  }
30
30
  const result = fn[functionName](table, columnName, {
31
31
  type,
32
- query: options?.query,
33
- cases: options?.cases,
32
+ relationalCountOptions: isFunctionColumnOptions(options)
33
+ ? {
34
+ query: options.query,
35
+ cases: options.cases,
36
+ permissions: options.permissions,
37
+ }
38
+ : undefined,
34
39
  originalCollectionName: options?.originalCollectionName,
35
40
  });
36
41
  if (alias) {
@@ -47,3 +52,6 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
47
52
  }
48
53
  return knex.ref(`${table}.${column}`);
49
54
  }
55
+ function isFunctionColumnOptions(options) {
56
+ return !!options && 'query' in options;
57
+ }