directus 9.8.0 → 9.10.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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/__mocks__/cache.d.ts +5 -0
  3. package/dist/__mocks__/cache.js +7 -0
  4. package/dist/app.js +3 -0
  5. package/dist/auth/drivers/ldap.js +10 -11
  6. package/dist/auth/drivers/oauth2.js +11 -6
  7. package/dist/auth/drivers/openid.js +9 -6
  8. package/dist/cli/commands/schema/apply.js +9 -3
  9. package/dist/controllers/assets.js +5 -0
  10. package/dist/controllers/files.d.ts +2 -0
  11. package/dist/controllers/files.js +13 -5
  12. package/dist/database/helpers/date/dialects/mssql.d.ts +4 -0
  13. package/dist/database/helpers/date/dialects/mssql.js +12 -0
  14. package/dist/database/helpers/date/dialects/mysql.d.ts +5 -0
  15. package/dist/database/helpers/date/dialects/mysql.js +16 -0
  16. package/dist/database/helpers/date/dialects/oracle.d.ts +4 -0
  17. package/dist/database/helpers/date/dialects/oracle.js +15 -0
  18. package/dist/database/helpers/date/dialects/sqlite.d.ts +1 -0
  19. package/dist/database/helpers/date/dialects/sqlite.js +8 -0
  20. package/dist/database/helpers/date/index.d.ts +3 -3
  21. package/dist/database/helpers/date/index.js +6 -6
  22. package/dist/database/helpers/date/types.d.ts +3 -0
  23. package/dist/database/helpers/date/types.js +10 -0
  24. package/dist/database/helpers/fn/dialects/postgres.js +5 -1
  25. package/dist/database/helpers/index.d.ts +1 -1
  26. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.d.ts +3 -0
  27. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +22 -0
  28. package/dist/database/run-ast.js +3 -3
  29. package/dist/database/system-data/fields/collections.yaml +1 -1
  30. package/dist/database/system-data/fields/settings.yaml +0 -1
  31. package/dist/database/system-data/fields/users.yaml +3 -0
  32. package/dist/env.js +9 -3
  33. package/dist/exceptions/index.d.ts +1 -0
  34. package/dist/exceptions/index.js +1 -0
  35. package/dist/exceptions/token-expired.d.ts +4 -0
  36. package/dist/exceptions/token-expired.js +10 -0
  37. package/dist/middleware/cache.js +10 -0
  38. package/dist/services/authorization.js +72 -30
  39. package/dist/services/collections.d.ts +2 -0
  40. package/dist/services/collections.js +10 -0
  41. package/dist/services/fields.js +26 -1
  42. package/dist/services/files.d.ts +5 -1
  43. package/dist/services/files.js +59 -40
  44. package/dist/services/graphql.d.ts +2 -3
  45. package/dist/services/graphql.js +65 -11
  46. package/dist/services/import-export.js +2 -0
  47. package/dist/services/items.js +12 -5
  48. package/dist/services/payload.d.ts +2 -1
  49. package/dist/services/payload.js +22 -17
  50. package/dist/services/specifications.js +1 -3
  51. package/dist/services/users.js +4 -1
  52. package/dist/types/files.d.ts +8 -0
  53. package/dist/utils/apply-query.d.ts +2 -1
  54. package/dist/utils/apply-query.js +134 -156
  55. package/dist/utils/apply-snapshot.d.ts +3 -1
  56. package/dist/utils/apply-snapshot.js +34 -5
  57. package/dist/utils/get-ast-from-query.js +15 -3
  58. package/dist/utils/get-column-path.d.ts +16 -0
  59. package/dist/utils/get-column-path.js +46 -0
  60. package/dist/utils/get-graphql-type.js +1 -0
  61. package/dist/utils/get-local-type.js +5 -0
  62. package/dist/utils/get-relation-info.d.ts +7 -0
  63. package/dist/utils/get-relation-info.js +45 -0
  64. package/dist/utils/get-relation-type.d.ts +1 -1
  65. package/dist/utils/get-schema.js +3 -0
  66. package/dist/utils/jwt.js +1 -1
  67. package/dist/utils/merge-permissions-for-share.js +1 -1
  68. package/dist/utils/reduce-schema.js +18 -11
  69. package/dist/utils/validate-query.js +19 -15
  70. package/example.env +4 -0
  71. package/package.json +18 -19
@@ -25,6 +25,7 @@ __exportStar(require("./method-not-allowed"), exports);
25
25
  __exportStar(require("./range-not-satisfiable"), exports);
26
26
  __exportStar(require("./route-not-found"), exports);
27
27
  __exportStar(require("./service-unavailable"), exports);
28
+ __exportStar(require("./token-expired"), exports);
28
29
  __exportStar(require("./unprocessable-entity"), exports);
29
30
  __exportStar(require("./unsupported-media-type"), exports);
30
31
  __exportStar(require("./user-suspended"), exports);
@@ -0,0 +1,4 @@
1
+ import { BaseException } from '@directus/shared/exceptions';
2
+ export declare class TokenExpiredException extends BaseException {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenExpiredException = void 0;
4
+ const exceptions_1 = require("@directus/shared/exceptions");
5
+ class TokenExpiredException extends exceptions_1.BaseException {
6
+ constructor(message = 'Token expired.') {
7
+ super(message, 401, 'TOKEN_EXPIRED');
8
+ }
9
+ }
10
+ exports.TokenExpiredException = TokenExpiredException;
@@ -19,6 +19,8 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
19
19
  if (!cache)
20
20
  return next();
21
21
  if (((_a = req.headers['cache-control']) === null || _a === void 0 ? void 0 : _a.includes('no-store')) || ((_b = req.headers['Cache-Control']) === null || _b === void 0 ? void 0 : _b.includes('no-store'))) {
22
+ if (env_1.default.CACHE_STATUS_HEADER)
23
+ res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
22
24
  return next();
23
25
  }
24
26
  const key = (0, get_cache_key_1.getCacheKey)(req);
@@ -28,6 +30,8 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
28
30
  }
29
31
  catch (err) {
30
32
  logger_1.default.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`);
33
+ if (env_1.default.CACHE_STATUS_HEADER)
34
+ res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
31
35
  return next();
32
36
  }
33
37
  if (cachedData) {
@@ -37,14 +41,20 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
37
41
  }
38
42
  catch (err) {
39
43
  logger_1.default.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`);
44
+ if (env_1.default.CACHE_STATUS_HEADER)
45
+ res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
40
46
  return next();
41
47
  }
42
48
  const cacheTTL = cacheExpiryDate ? cacheExpiryDate - Date.now() : null;
43
49
  res.setHeader('Cache-Control', (0, get_cache_headers_1.getCacheControlHeader)(req, cacheTTL));
44
50
  res.setHeader('Vary', 'Origin, Cache-Control');
51
+ if (env_1.default.CACHE_STATUS_HEADER)
52
+ res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'HIT');
45
53
  return res.json(cachedData);
46
54
  }
47
55
  else {
56
+ if (env_1.default.CACHE_STATUS_HEADER)
57
+ res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
48
58
  return next();
49
59
  }
50
60
  });
@@ -35,7 +35,7 @@ class AuthorizationService {
35
35
  throw new exceptions_2.ForbiddenException();
36
36
  }
37
37
  validateFields(ast);
38
- validateFilterPermissions(ast, this.schema, this.accountability);
38
+ validateFilterPermissions(ast, this.schema, action, this.accountability);
39
39
  applyFilters(ast, this.accountability);
40
40
  return ast;
41
41
  /**
@@ -87,6 +87,8 @@ class AuthorizationService {
87
87
  if (!aliasMap)
88
88
  continue;
89
89
  for (const column of Object.values(aliasMap)) {
90
+ if (column === '*')
91
+ continue;
90
92
  if (allowedFields.includes(column) === false)
91
93
  throw new exceptions_2.ForbiddenException();
92
94
  }
@@ -106,7 +108,7 @@ class AuthorizationService {
106
108
  }
107
109
  }
108
110
  }
109
- function validateFilterPermissions(ast, schema, accountability) {
111
+ function validateFilterPermissions(ast, schema, action, accountability) {
110
112
  var _a, _b, _c, _d, _e;
111
113
  let requiredFieldPermissions = {};
112
114
  if (ast.type !== 'field') {
@@ -114,28 +116,34 @@ class AuthorizationService {
114
116
  for (const collection of Object.keys(ast.children)) {
115
117
  requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, extractRequiredFieldPermissions(collection, (_c = (_b = (_a = ast.query) === null || _a === void 0 ? void 0 : _a[collection]) === null || _b === void 0 ? void 0 : _b.filter) !== null && _c !== void 0 ? _c : {}));
116
118
  for (const child of ast.children[collection]) {
117
- // Always add relational field as a deep child may have a filter
118
- if (child.type !== 'field') {
119
- (requiredFieldPermissions[collection] || (requiredFieldPermissions[collection] = new Set())).add(child.fieldKey);
119
+ const childPermissions = validateFilterPermissions(child, schema, action, accountability);
120
+ if (Object.keys(childPermissions).length > 0) {
121
+ //Only add relational field if deep child has a filter
122
+ if (child.type !== 'field') {
123
+ (requiredFieldPermissions[collection] || (requiredFieldPermissions[collection] = new Set())).add(child.fieldKey);
124
+ }
125
+ requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, childPermissions);
120
126
  }
121
- requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, validateFilterPermissions(child, schema, accountability));
122
127
  }
123
128
  }
124
129
  }
125
130
  else {
126
131
  requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, extractRequiredFieldPermissions(ast.name, (_e = (_d = ast.query) === null || _d === void 0 ? void 0 : _d.filter) !== null && _e !== void 0 ? _e : {}));
127
132
  for (const child of ast.children) {
128
- // Always add relational field as a deep child may have a filter
129
- if (child.type !== 'field') {
130
- (requiredFieldPermissions[ast.name] || (requiredFieldPermissions[ast.name] = new Set())).add(child.fieldKey);
133
+ const childPermissions = validateFilterPermissions(child, schema, action, accountability);
134
+ if (Object.keys(childPermissions).length > 0) {
135
+ // Only add relational field if deep child has a filter
136
+ if (child.type !== 'field') {
137
+ (requiredFieldPermissions[ast.name] || (requiredFieldPermissions[ast.name] = new Set())).add(child.fieldKey);
138
+ }
139
+ requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, childPermissions);
131
140
  }
132
- requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, validateFilterPermissions(child, schema, accountability));
133
141
  }
134
142
  }
135
143
  }
136
144
  if (ast.type === 'root') {
137
145
  // Validate all required permissions once at the root level
138
- checkFieldPermissions(requiredFieldPermissions);
146
+ checkFieldPermissions(ast.name, schema, action, requiredFieldPermissions);
139
147
  }
140
148
  return requiredFieldPermissions;
141
149
  function extractRequiredFieldPermissions(collection, filter, parentCollection, parentField) {
@@ -153,6 +161,34 @@ class AuthorizationService {
153
161
  // Filter value is not a filter, so we should skip it
154
162
  return result;
155
163
  }
164
+ // m2a filter in the form of `item:collection`
165
+ else if (filterKey.includes(':')) {
166
+ const [field, collectionScope] = filterKey.split(':');
167
+ if (collection) {
168
+ // Add the `item` field to the required permissions
169
+ (result[collection] || (result[collection] = new Set())).add(field);
170
+ // Add the `collection` field to the required permissions
171
+ result[collection].add('collection');
172
+ }
173
+ else {
174
+ const relation = schema.relations.find((relation) => {
175
+ var _a;
176
+ return ((relation.collection === parentCollection && relation.field === parentField) ||
177
+ (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === parentField));
178
+ });
179
+ // Filter key not found in parent collection
180
+ if (!relation)
181
+ throw new exceptions_2.ForbiddenException();
182
+ const relatedCollectionName = relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
183
+ // Add the `item` field to the required permissions
184
+ (result[relatedCollectionName] || (result[relatedCollectionName] = new Set())).add(field);
185
+ // Add the `collection` field to the required permissions
186
+ result[relatedCollectionName].add('collection');
187
+ }
188
+ // Continue to parse the filter for nested `collection` afresh
189
+ const requiredPermissions = extractRequiredFieldPermissions(collectionScope, filterValue);
190
+ result = mergeRequiredFieldPermissions(result, requiredPermissions);
191
+ }
156
192
  else {
157
193
  if (collection) {
158
194
  (result[collection] || (result[collection] = new Set())).add(filterKey);
@@ -163,20 +199,12 @@ class AuthorizationService {
163
199
  return ((relation.collection === parentCollection && relation.field === parentField) ||
164
200
  (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === parentField));
165
201
  });
166
- if (relation) {
167
- if (relation.related_collection === parentCollection) {
168
- (result[relation.collection] || (result[relation.collection] = new Set())).add(filterKey);
169
- parentCollection = relation.collection;
170
- }
171
- else {
172
- (result[relation.related_collection] || (result[relation.related_collection] = new Set())).add(filterKey);
173
- parentCollection = relation.related_collection;
174
- }
175
- }
176
- else {
177
- // Filter key not found in parent collection
202
+ // Filter key not found in parent collection
203
+ if (!relation)
178
204
  throw new exceptions_2.ForbiddenException();
179
- }
205
+ parentCollection =
206
+ relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
207
+ (result[parentCollection] || (result[parentCollection] = new Set())).add(filterKey);
180
208
  }
181
209
  if (typeof filterValue === 'object') {
182
210
  // Parent collection is undefined when we process the top level filter
@@ -214,20 +242,34 @@ class AuthorizationService {
214
242
  }
215
243
  return current;
216
244
  }
217
- function checkFieldPermissions(requiredPermissions) {
218
- var _a;
245
+ function checkFieldPermissions(rootCollection, schema, action, requiredPermissions) {
246
+ var _a, _b;
219
247
  if ((accountability === null || accountability === void 0 ? void 0 : accountability.admin) === true)
220
248
  return;
221
249
  for (const collection of Object.keys(requiredPermissions)) {
222
250
  const permission = (_a = accountability === null || accountability === void 0 ? void 0 : accountability.permissions) === null || _a === void 0 ? void 0 : _a.find((permission) => permission.collection === collection && permission.action === 'read');
223
- if (!permission || !permission.fields)
251
+ let allowedFields;
252
+ // Allow the filtering of top level ID for actions such as update and delete
253
+ if (action !== 'read' && collection === rootCollection) {
254
+ const actionPermission = (_b = accountability === null || accountability === void 0 ? void 0 : accountability.permissions) === null || _b === void 0 ? void 0 : _b.find((permission) => permission.collection === collection && permission.action === action);
255
+ if (!actionPermission || !actionPermission.fields) {
256
+ throw new exceptions_2.ForbiddenException();
257
+ }
258
+ allowedFields = (permission === null || permission === void 0 ? void 0 : permission.fields)
259
+ ? [...permission.fields, schema.collections[collection].primary]
260
+ : [schema.collections[collection].primary];
261
+ }
262
+ else if (!permission || !permission.fields) {
224
263
  throw new exceptions_2.ForbiddenException();
225
- const allowedFields = permission.fields;
264
+ }
265
+ else {
266
+ allowedFields = permission.fields;
267
+ }
226
268
  if (allowedFields.includes('*'))
227
269
  continue;
228
270
  // Allow legacy permissions with an empty fields array, where id can be accessed
229
271
  if (allowedFields.length === 0)
230
- allowedFields.push('id');
272
+ allowedFields.push(schema.collections[collection].primary);
231
273
  for (const field of requiredPermissions[collection]) {
232
274
  if (!allowedFields.includes(field))
233
275
  throw new exceptions_2.ForbiddenException();
@@ -342,7 +384,7 @@ class AuthorizationService {
342
384
  }
343
385
  }
344
386
  if (hasFieldValidationRules) {
345
- if (permission.validation) {
387
+ if (permission.validation && Object.keys(permission.validation).length > 0) {
346
388
  permission.validation = { _and: [permission.validation, ...fieldValidationRules] };
347
389
  }
348
390
  else {
@@ -4,6 +4,7 @@ import Keyv from 'keyv';
4
4
  import { AbstractServiceOptions, Collection, CollectionMeta, MutationOptions } from '../types';
5
5
  import { Accountability, RawField, SchemaOverview } from '@directus/shared/types';
6
6
  import { Table } from 'knex-schema-inspector/dist/types/table';
7
+ import { Helpers } from '../database/helpers';
7
8
  export declare type RawCollection = {
8
9
  collection: string;
9
10
  fields?: RawField[];
@@ -12,6 +13,7 @@ export declare type RawCollection = {
12
13
  };
13
14
  export declare class CollectionsService {
14
15
  knex: Knex;
16
+ helpers: Helpers;
15
17
  accountability: Accountability | null;
16
18
  schemaInspector: ReturnType<typeof SchemaInspector>;
17
19
  schema: SchemaOverview;
@@ -32,9 +32,12 @@ const env_1 = __importDefault(require("../env"));
32
32
  const exceptions_1 = require("../exceptions");
33
33
  const fields_1 = require("../services/fields");
34
34
  const items_1 = require("../services/items");
35
+ const utils_1 = require("@directus/shared/utils");
36
+ const helpers_1 = require("../database/helpers");
35
37
  class CollectionsService {
36
38
  constructor(options) {
37
39
  this.knex = options.knex || (0, database_1.default)();
40
+ this.helpers = (0, helpers_1.getHelpers)(this.knex);
38
41
  this.accountability = options.accountability || null;
39
42
  this.schemaInspector = options.knex ? (0, schema_1.default)(options.knex) : (0, database_1.getSchemaInspector)();
40
43
  this.schema = options.schema;
@@ -96,6 +99,11 @@ class CollectionsService {
96
99
  collection: payload.collection,
97
100
  };
98
101
  }
102
+ // Add flag for specific database type overrides
103
+ const flagToAdd = this.helpers.date.fieldFlagForField(field.type);
104
+ if (flagToAdd) {
105
+ (0, utils_1.addFieldFlag)(field, flagToAdd);
106
+ }
99
107
  return field;
100
108
  });
101
109
  const fieldsService = new fields_1.FieldsService({ knex: trx, schema: this.schema });
@@ -231,6 +239,8 @@ class CollectionsService {
231
239
  */
232
240
  async readOne(collectionKey) {
233
241
  const result = await this.readMany([collectionKey]);
242
+ if (result.length === 0)
243
+ throw new exceptions_1.ForbiddenException();
234
244
  return result[0];
235
245
  }
236
246
  /**
@@ -40,6 +40,7 @@ const utils_1 = require("@directus/shared/utils");
40
40
  const lodash_1 = require("lodash");
41
41
  const relations_1 = require("./relations");
42
42
  const helpers_1 = require("../database/helpers");
43
+ const constants_2 = require("@directus/shared/constants");
43
44
  class FieldsService {
44
45
  constructor(options) {
45
46
  this.knex = options.knex || (0, database_1.default)();
@@ -60,6 +61,7 @@ class FieldsService {
60
61
  }));
61
62
  }
62
63
  async readAll(collection) {
64
+ var _a, _b, _c, _d;
63
65
  let fields;
64
66
  if (this.accountability && this.accountability.admin !== true && this.hasReadAccess === false) {
65
67
  throw new exceptions_1.ForbiddenException();
@@ -151,6 +153,15 @@ class FieldsService {
151
153
  return allowedFields.includes(field.field);
152
154
  });
153
155
  }
156
+ // Update specific database type overrides
157
+ for (const field of result) {
158
+ if ((_b = (_a = field.meta) === null || _a === void 0 ? void 0 : _a.special) === null || _b === void 0 ? void 0 : _b.includes('cast-timestamp')) {
159
+ field.type = 'timestamp';
160
+ }
161
+ else if ((_d = (_c = field.meta) === null || _c === void 0 ? void 0 : _c.special) === null || _d === void 0 ? void 0 : _d.includes('cast-datetime')) {
162
+ field.type = 'dateTime';
163
+ }
164
+ }
154
165
  return result;
155
166
  }
156
167
  async readOne(collection, field) {
@@ -184,6 +195,8 @@ class FieldsService {
184
195
  catch {
185
196
  // Do nothing
186
197
  }
198
+ if (!column && !fieldInfo)
199
+ throw new exceptions_1.ForbiddenException();
187
200
  const type = (0, get_local_type_1.default)(column, fieldInfo);
188
201
  const data = {
189
202
  collection,
@@ -206,6 +219,11 @@ class FieldsService {
206
219
  if (exists) {
207
220
  throw new exceptions_1.InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
208
221
  }
222
+ // Add flag for specific database type overrides
223
+ const flagToAdd = this.helpers.date.fieldFlagForField(field.type);
224
+ if (flagToAdd) {
225
+ (0, utils_1.addFieldFlag)(field, flagToAdd);
226
+ }
209
227
  await this.knex.transaction(async (trx) => {
210
228
  const itemsService = new items_1.ItemsService('directus_fields', {
211
229
  knex: trx,
@@ -452,9 +470,16 @@ class FieldsService {
452
470
  column = table[field.type](field.field);
453
471
  }
454
472
  if (((_f = field.schema) === null || _f === void 0 ? void 0 : _f.default_value) !== undefined) {
455
- if (typeof field.schema.default_value === 'string' && field.schema.default_value.toLowerCase() === 'now()') {
473
+ if (typeof field.schema.default_value === 'string' &&
474
+ (field.schema.default_value.toLowerCase() === 'now()' || field.schema.default_value === 'CURRENT_TIMESTAMP')) {
456
475
  column.defaultTo(this.knex.fn.now());
457
476
  }
477
+ else if (typeof field.schema.default_value === 'string' &&
478
+ field.schema.default_value.includes('CURRENT_TIMESTAMP(') &&
479
+ field.schema.default_value.includes(')')) {
480
+ const precision = field.schema.default_value.match(constants_2.REGEX_BETWEEN_PARENS)[1];
481
+ column.defaultTo(this.knex.fn.now(Number(precision)));
482
+ }
458
483
  else if (typeof field.schema.default_value === 'string' &&
459
484
  ['"null"', 'null'].includes(field.schema.default_value.toLowerCase())) {
460
485
  column.defaultTo(null);
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { AbstractServiceOptions, File, PrimaryKey, MutationOptions } from '../types';
2
+ import { AbstractServiceOptions, File, PrimaryKey, MutationOptions, Metadata } from '../types';
3
3
  import { ItemsService } from './items';
4
4
  export declare class FilesService extends ItemsService {
5
5
  constructor(options: AbstractServiceOptions);
@@ -10,6 +10,10 @@ export declare class FilesService extends ItemsService {
10
10
  filename_download: string;
11
11
  storage: string;
12
12
  }, primaryKey?: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey>;
13
+ /**
14
+ * Extract metadata from a buffer's content
15
+ */
16
+ getMetadata(bufferContent: any, allowList?: any): Promise<Metadata>;
13
17
  /**
14
18
  * Import a single file from an external URL
15
19
  */
@@ -88,46 +88,13 @@ class FilesService extends items_1.ItemsService {
88
88
  payload.filesize = size;
89
89
  if (['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/tiff'].includes(payload.type)) {
90
90
  const buffer = await storage_1.default.disk(data.storage).getBuffer(payload.filename_disk);
91
- try {
92
- const meta = await (0, sharp_1.default)(buffer.content, {}).metadata();
93
- if (meta.orientation && meta.orientation >= 5) {
94
- payload.height = meta.width;
95
- payload.width = meta.height;
96
- }
97
- else {
98
- payload.width = meta.width;
99
- payload.height = meta.height;
100
- }
101
- }
102
- catch (err) {
103
- logger_1.default.warn(`Couldn't extract sharp metadata from file`);
104
- logger_1.default.warn(err);
105
- }
106
- payload.metadata = {};
107
- try {
108
- payload.metadata = await exifr_1.default.parse(buffer.content, {
109
- icc: false,
110
- iptc: true,
111
- ifd1: true,
112
- interop: true,
113
- translateValues: true,
114
- reviveValues: true,
115
- mergeOutput: false,
116
- });
117
- if ((_b = (_a = payload.metadata) === null || _a === void 0 ? void 0 : _a.iptc) === null || _b === void 0 ? void 0 : _b.Headline) {
118
- payload.title = payload.metadata.iptc.Headline;
119
- }
120
- if (!payload.description && ((_d = (_c = payload.metadata) === null || _c === void 0 ? void 0 : _c.iptc) === null || _d === void 0 ? void 0 : _d.Caption)) {
121
- payload.description = payload.metadata.iptc.Caption;
122
- }
123
- if ((_f = (_e = payload.metadata) === null || _e === void 0 ? void 0 : _e.iptc) === null || _f === void 0 ? void 0 : _f.Keywords) {
124
- payload.tags = payload.metadata.iptc.Keywords;
125
- }
126
- }
127
- catch (err) {
128
- logger_1.default.warn(`Couldn't extract EXIF metadata from file`);
129
- logger_1.default.warn(err);
130
- }
91
+ const { height, width, description, title, tags, metadata } = await this.getMetadata(buffer.content);
92
+ (_a = payload.height) !== null && _a !== void 0 ? _a : (payload.height = height);
93
+ (_b = payload.width) !== null && _b !== void 0 ? _b : (payload.width = width);
94
+ (_c = payload.description) !== null && _c !== void 0 ? _c : (payload.description = description);
95
+ (_d = payload.title) !== null && _d !== void 0 ? _d : (payload.title = title);
96
+ (_e = payload.tags) !== null && _e !== void 0 ? _e : (payload.tags = tags);
97
+ (_f = payload.metadata) !== null && _f !== void 0 ? _f : (payload.metadata = metadata);
131
98
  }
132
99
  // We do this in a service without accountability. Even if you don't have update permissions to the file,
133
100
  // we still want to be able to set the extracted values from the file on create
@@ -152,6 +119,58 @@ class FilesService extends items_1.ItemsService {
152
119
  }
153
120
  return primaryKey;
154
121
  }
122
+ /**
123
+ * Extract metadata from a buffer's content
124
+ */
125
+ async getMetadata(bufferContent, allowList = env_1.default.FILE_METADATA_ALLOW_LIST) {
126
+ const metadata = {};
127
+ try {
128
+ const sharpMetadata = await (0, sharp_1.default)(bufferContent, {}).metadata();
129
+ if (sharpMetadata.orientation && sharpMetadata.orientation >= 5) {
130
+ metadata.height = sharpMetadata.width;
131
+ metadata.width = sharpMetadata.height;
132
+ }
133
+ else {
134
+ metadata.width = sharpMetadata.width;
135
+ metadata.height = sharpMetadata.height;
136
+ }
137
+ }
138
+ catch (err) {
139
+ logger_1.default.warn(`Couldn't extract sharp metadata from file`);
140
+ logger_1.default.warn(err);
141
+ }
142
+ try {
143
+ const exifrMetadata = await exifr_1.default.parse(bufferContent, {
144
+ icc: false,
145
+ iptc: true,
146
+ ifd1: true,
147
+ interop: true,
148
+ translateValues: true,
149
+ reviveValues: true,
150
+ mergeOutput: false,
151
+ });
152
+ if (allowList === '*' || (allowList === null || allowList === void 0 ? void 0 : allowList[0]) === '*') {
153
+ metadata.metadata = exifrMetadata;
154
+ }
155
+ else {
156
+ metadata.metadata = (0, lodash_1.pick)(exifrMetadata, allowList);
157
+ }
158
+ if (!metadata.description && (exifrMetadata === null || exifrMetadata === void 0 ? void 0 : exifrMetadata.Caption)) {
159
+ metadata.description = exifrMetadata.Caption;
160
+ }
161
+ if (exifrMetadata === null || exifrMetadata === void 0 ? void 0 : exifrMetadata.Headline) {
162
+ metadata.title = exifrMetadata.Headline;
163
+ }
164
+ if (exifrMetadata === null || exifrMetadata === void 0 ? void 0 : exifrMetadata.Keywords) {
165
+ metadata.tags = exifrMetadata.Keywords;
166
+ }
167
+ }
168
+ catch (err) {
169
+ logger_1.default.warn(`Couldn't extract EXIF metadata from file`);
170
+ logger_1.default.warn(err);
171
+ }
172
+ return metadata;
173
+ }
155
174
  /**
156
175
  * Import a single file from an external URL
157
176
  */
@@ -1,9 +1,8 @@
1
+ import { BaseException } from '@directus/shared/exceptions';
2
+ import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
1
3
  import { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLError, GraphQLResolveInfo, GraphQLScalarType, GraphQLSchema, ObjectFieldNode, SelectionNode } from 'graphql';
2
- import { SchemaOverview } from '@directus/shared/types';
3
4
  import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose';
4
5
  import { Knex } from 'knex';
5
- import { BaseException } from '@directus/shared/exceptions';
6
- import { Accountability, Query } from '@directus/shared/types';
7
6
  import { AbstractServiceOptions, GraphQLParams, Item } from '../types';
8
7
  import { ItemsService } from './items';
9
8
  export declare const GraphQLGeoJSON: GraphQLScalarType;