directus 9.9.0 → 9.11.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 (132) hide show
  1. package/README.md +1 -1
  2. package/dist/app.js +3 -0
  3. package/dist/auth/drivers/oauth2.d.ts +1 -1
  4. package/dist/auth/drivers/oauth2.js +14 -11
  5. package/dist/auth/drivers/openid.d.ts +1 -1
  6. package/dist/auth/drivers/openid.js +14 -11
  7. package/dist/cli/commands/schema/apply.js +4 -3
  8. package/dist/controllers/assets.js +8 -9
  9. package/dist/controllers/files.js +3 -0
  10. package/dist/database/helpers/date/dialects/sqlite.js +6 -2
  11. package/dist/database/helpers/fn/dialects/postgres.js +5 -1
  12. package/dist/database/migrations/20210225A-add-relations-sort-field.js +2 -1
  13. package/dist/database/migrations/20210506A-rename-interfaces.js +2 -1
  14. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  15. package/dist/database/migrations/20210805A-update-groups.js +2 -1
  16. package/dist/database/migrations/20210805B-change-image-metadata-structure.js +3 -2
  17. package/dist/database/migrations/20211007A-update-presets.js +5 -4
  18. package/dist/database/run-ast.js +11 -15
  19. package/dist/database/system-data/fields/collections.yaml +1 -1
  20. package/dist/env.js +8 -3
  21. package/dist/exceptions/index.d.ts +1 -0
  22. package/dist/exceptions/index.js +1 -0
  23. package/dist/exceptions/invalid-provider.d.ts +4 -0
  24. package/dist/exceptions/invalid-provider.js +10 -0
  25. package/dist/exceptions/range-not-satisfiable.d.ts +2 -2
  26. package/dist/exceptions/range-not-satisfiable.js +5 -1
  27. package/dist/middleware/graphql.js +2 -1
  28. package/dist/services/assets.js +27 -1
  29. package/dist/services/authentication.js +4 -1
  30. package/dist/services/collections.js +2 -0
  31. package/dist/services/fields.js +17 -8
  32. package/dist/services/graphql.js +75 -34
  33. package/dist/services/import-export.d.ts +1 -1
  34. package/dist/services/import-export.js +14 -11
  35. package/dist/services/items.d.ts +3 -3
  36. package/dist/services/items.js +20 -6
  37. package/dist/services/payload.d.ts +3 -3
  38. package/dist/services/payload.js +11 -8
  39. package/dist/services/specifications.js +1 -3
  40. package/dist/services/users.d.ts +4 -0
  41. package/dist/services/users.js +24 -1
  42. package/dist/utils/{apply-query.d.ts → apply-query/index.d.ts} +2 -1
  43. package/dist/utils/apply-query/index.js +394 -0
  44. package/dist/utils/apply-query/operators/between.operator.d.ts +2 -0
  45. package/dist/utils/apply-query/operators/between.operator.js +16 -0
  46. package/dist/utils/apply-query/operators/contains.operator.d.ts +2 -0
  47. package/dist/utils/apply-query/operators/contains.operator.js +9 -0
  48. package/dist/utils/apply-query/operators/ends-with.operator.d.ts +2 -0
  49. package/dist/utils/apply-query/operators/ends-with.operator.js +9 -0
  50. package/dist/utils/apply-query/operators/equals.operator.d.ts +2 -0
  51. package/dist/utils/apply-query/operators/equals.operator.js +9 -0
  52. package/dist/utils/apply-query/operators/greather-than-equals.operator.d.ts +2 -0
  53. package/dist/utils/apply-query/operators/greather-than-equals.operator.js +9 -0
  54. package/dist/utils/apply-query/operators/greather-than.operator.d.ts +2 -0
  55. package/dist/utils/apply-query/operators/greather-than.operator.js +9 -0
  56. package/dist/utils/apply-query/operators/in.operator.d.ts +2 -0
  57. package/dist/utils/apply-query/operators/in.operator.js +14 -0
  58. package/dist/utils/apply-query/operators/index.d.ts +3 -0
  59. package/dist/utils/apply-query/operators/index.js +72 -0
  60. package/dist/utils/apply-query/operators/insensitive-contains.operator.d.ts +2 -0
  61. package/dist/utils/apply-query/operators/insensitive-contains.operator.js +9 -0
  62. package/dist/utils/apply-query/operators/insensitive-ends-with.operator.d.ts +2 -0
  63. package/dist/utils/apply-query/operators/insensitive-ends-with.operator.js +9 -0
  64. package/dist/utils/apply-query/operators/insensitive-equals.operator.d.ts +2 -0
  65. package/dist/utils/apply-query/operators/insensitive-equals.operator.js +9 -0
  66. package/dist/utils/apply-query/operators/insensitive-not-contains.operator.d.ts +2 -0
  67. package/dist/utils/apply-query/operators/insensitive-not-contains.operator.js +9 -0
  68. package/dist/utils/apply-query/operators/insensitive-not-ends-with.operator.d.ts +2 -0
  69. package/dist/utils/apply-query/operators/insensitive-not-ends-with.operator.js +9 -0
  70. package/dist/utils/apply-query/operators/insensitive-not-equals.operator.d.ts +2 -0
  71. package/dist/utils/apply-query/operators/insensitive-not-equals.operator.js +9 -0
  72. package/dist/utils/apply-query/operators/insensitive-not-starts-with.operator.d.ts +2 -0
  73. package/dist/utils/apply-query/operators/insensitive-not-starts-with.operator.js +9 -0
  74. package/dist/utils/apply-query/operators/insensitive-starts-with.operator.d.ts +2 -0
  75. package/dist/utils/apply-query/operators/insensitive-starts-with.operator.js +9 -0
  76. package/dist/utils/apply-query/operators/intersects-bbox.operator.d.ts +2 -0
  77. package/dist/utils/apply-query/operators/intersects-bbox.operator.js +9 -0
  78. package/dist/utils/apply-query/operators/intersects.operator.d.ts +2 -0
  79. package/dist/utils/apply-query/operators/intersects.operator.js +9 -0
  80. package/dist/utils/apply-query/operators/is-empty.operator.d.ts +2 -0
  81. package/dist/utils/apply-query/operators/is-empty.operator.js +14 -0
  82. package/dist/utils/apply-query/operators/is-not-empty.operator.d.ts +2 -0
  83. package/dist/utils/apply-query/operators/is-not-empty.operator.js +14 -0
  84. package/dist/utils/apply-query/operators/is-not-null.operator.d.ts +2 -0
  85. package/dist/utils/apply-query/operators/is-not-null.operator.js +14 -0
  86. package/dist/utils/apply-query/operators/is-null.operator.d.ts +2 -0
  87. package/dist/utils/apply-query/operators/is-null.operator.js +14 -0
  88. package/dist/utils/apply-query/operators/less-than-equals.operator.d.ts +2 -0
  89. package/dist/utils/apply-query/operators/less-than-equals.operator.js +9 -0
  90. package/dist/utils/apply-query/operators/less-than.operator.d.ts +2 -0
  91. package/dist/utils/apply-query/operators/less-than.operator.js +9 -0
  92. package/dist/utils/apply-query/operators/not-between.operator.d.ts +2 -0
  93. package/dist/utils/apply-query/operators/not-between.operator.js +16 -0
  94. package/dist/utils/apply-query/operators/not-contains.operator.d.ts +2 -0
  95. package/dist/utils/apply-query/operators/not-contains.operator.js +9 -0
  96. package/dist/utils/apply-query/operators/not-ends-with.operator.d.ts +2 -0
  97. package/dist/utils/apply-query/operators/not-ends-with.operator.js +9 -0
  98. package/dist/utils/apply-query/operators/not-equals.operator.d.ts +2 -0
  99. package/dist/utils/apply-query/operators/not-equals.operator.js +9 -0
  100. package/dist/utils/apply-query/operators/not-in.operator.d.ts +2 -0
  101. package/dist/utils/apply-query/operators/not-in.operator.js +14 -0
  102. package/dist/utils/apply-query/operators/not-intersects-bbox.operator.d.ts +2 -0
  103. package/dist/utils/apply-query/operators/not-intersects-bbox.operator.js +9 -0
  104. package/dist/utils/apply-query/operators/not-intersects.operator.d.ts +2 -0
  105. package/dist/utils/apply-query/operators/not-intersects.operator.js +9 -0
  106. package/dist/utils/apply-query/operators/not-starts-with.operator.d.ts +2 -0
  107. package/dist/utils/apply-query/operators/not-starts-with.operator.js +9 -0
  108. package/dist/utils/apply-query/operators/operator-register.d.ts +13 -0
  109. package/dist/utils/apply-query/operators/operator-register.js +7 -0
  110. package/dist/utils/apply-query/operators/starts-with.operator.d.ts +2 -0
  111. package/dist/utils/apply-query/operators/starts-with.operator.js +9 -0
  112. package/dist/utils/apply-snapshot.d.ts +3 -3
  113. package/dist/utils/apply-snapshot.js +64 -49
  114. package/dist/utils/get-ast-from-query.js +10 -4
  115. package/dist/utils/get-column-path.d.ts +16 -0
  116. package/dist/utils/get-column-path.js +46 -0
  117. package/dist/utils/get-default-value.js +4 -3
  118. package/dist/utils/get-permissions.d.ts +1 -1
  119. package/dist/utils/get-permissions.js +9 -8
  120. package/dist/utils/get-relation-info.d.ts +7 -0
  121. package/dist/utils/get-relation-info.js +45 -0
  122. package/dist/utils/get-relation-type.d.ts +1 -1
  123. package/dist/utils/get-schema.js +5 -1
  124. package/dist/utils/get-snapshot.js +22 -4
  125. package/dist/utils/merge-permissions-for-share.js +1 -1
  126. package/dist/utils/parse-json.d.ts +5 -0
  127. package/dist/utils/parse-json.js +19 -0
  128. package/dist/utils/reduce-schema.js +18 -11
  129. package/dist/utils/sanitize-query.d.ts +1 -2
  130. package/dist/utils/sanitize-query.js +6 -5
  131. package/package.json +16 -18
  132. package/dist/utils/apply-query.js +0 -498
@@ -7,6 +7,7 @@ exports.parseGraphQL = void 0;
7
7
  const graphql_1 = require("graphql");
8
8
  const exceptions_1 = require("../exceptions");
9
9
  const async_handler_1 = __importDefault(require("../utils/async-handler"));
10
+ const parse_json_1 = require("../utils/parse-json");
10
11
  exports.parseGraphQL = (0, async_handler_1.default)(async (req, res, next) => {
11
12
  if (req.method !== 'GET' && req.method !== 'POST') {
12
13
  throw new exceptions_1.MethodNotAllowedException('GraphQL only supports GET and POST requests.', { allow: ['GET', 'POST'] });
@@ -19,7 +20,7 @@ exports.parseGraphQL = (0, async_handler_1.default)(async (req, res, next) => {
19
20
  query = req.query.query || null;
20
21
  if (req.query.variables) {
21
22
  try {
22
- variables = JSON.parse(req.query.variables);
23
+ variables = (0, parse_json_1.parseJSON)(req.query.variables);
23
24
  }
24
25
  catch {
25
26
  throw new exceptions_1.InvalidQueryException(`Variables are invalid JSON.`);
@@ -70,9 +70,35 @@ class AssetsService {
70
70
  if (!exists)
71
71
  throw new exceptions_1.ForbiddenException();
72
72
  if (range) {
73
- if (range.start >= file.filesize || (range.end && range.end >= file.filesize)) {
73
+ const missingRangeLimits = range.start === undefined && range.end === undefined;
74
+ const endBeforeStart = range.start !== undefined && range.end !== undefined && range.end <= range.start;
75
+ const startOverflow = range.start !== undefined && range.start >= file.filesize;
76
+ const endUnderflow = range.end !== undefined && range.end <= 0;
77
+ if (missingRangeLimits || endBeforeStart || startOverflow || endUnderflow) {
74
78
  throw new exceptions_1.RangeNotSatisfiableException(range);
75
79
  }
80
+ const lastByte = file.filesize - 1;
81
+ if (range.end) {
82
+ if (range.start === undefined) {
83
+ // fetch chunk from tail
84
+ range.start = file.filesize - range.end;
85
+ range.end = lastByte;
86
+ }
87
+ if (range.end >= file.filesize) {
88
+ // fetch entire file
89
+ range.end = lastByte;
90
+ }
91
+ }
92
+ if (range.start) {
93
+ if (range.end === undefined) {
94
+ // fetch entire file
95
+ range.end = lastByte;
96
+ }
97
+ if (range.start < 0) {
98
+ // fetch file from head
99
+ range.start = 0;
100
+ }
101
+ }
76
102
  }
77
103
  const type = file.type;
78
104
  const transforms = TransformationUtils.resolvePreset(transformation, file);
@@ -45,7 +45,6 @@ class AuthenticationService {
45
45
  .from('directus_users as u')
46
46
  .leftJoin('directus_roles as r', 'u.role', 'r.id')
47
47
  .where('u.id', await provider.getUserID((0, lodash_1.cloneDeep)(payload)))
48
- .andWhere('u.provider', providerName)
49
48
  .first();
50
49
  const updatedPayload = await emitter_1.default.emitFilter('auth.login', payload, {
51
50
  status: 'pending',
@@ -79,6 +78,10 @@ class AuthenticationService {
79
78
  throw new exceptions_1.InvalidCredentialsException();
80
79
  }
81
80
  }
81
+ else if (user.provider !== providerName) {
82
+ await (0, stall_1.stall)(STALL_TIME, timeStart);
83
+ throw new exceptions_1.InvalidProviderException();
84
+ }
82
85
  const settingsService = new settings_1.SettingsService({
83
86
  knex: this.knex,
84
87
  schema: this.schema,
@@ -239,6 +239,8 @@ class CollectionsService {
239
239
  */
240
240
  async readOne(collectionKey) {
241
241
  const result = await this.readMany([collectionKey]);
242
+ if (result.length === 0)
243
+ throw new exceptions_1.ForbiddenException();
242
244
  return result[0];
243
245
  }
244
246
  /**
@@ -195,6 +195,8 @@ class FieldsService {
195
195
  catch {
196
196
  // Do nothing
197
197
  }
198
+ if (!column && !fieldInfo)
199
+ throw new exceptions_1.ForbiddenException();
198
200
  const type = (0, get_local_type_1.default)(column, fieldInfo);
199
201
  const data = {
200
202
  collection,
@@ -349,13 +351,6 @@ class FieldsService {
349
351
  });
350
352
  await this.knex.transaction(async (trx) => {
351
353
  var _a, _b;
352
- if (this.schema.collections[collection] &&
353
- field in this.schema.collections[collection].fields &&
354
- this.schema.collections[collection].fields[field].alias === false) {
355
- await trx.schema.table(collection, (table) => {
356
- table.dropColumn(field);
357
- });
358
- }
359
354
  const relations = this.schema.relations.filter((relation) => {
360
355
  var _a;
361
356
  return ((relation.collection === collection && relation.field === field) ||
@@ -387,6 +382,14 @@ class FieldsService {
387
382
  .where({ many_collection: relation.collection, many_field: relation.field });
388
383
  }
389
384
  }
385
+ // Delete field only after foreign key constraints are removed
386
+ if (this.schema.collections[collection] &&
387
+ field in this.schema.collections[collection].fields &&
388
+ this.schema.collections[collection].fields[field].alias === false) {
389
+ await trx.schema.table(collection, (table) => {
390
+ table.dropColumn(field);
391
+ });
392
+ }
390
393
  const collectionMeta = await trx
391
394
  .select('archive_field', 'sort_field')
392
395
  .from('directus_collections')
@@ -439,7 +442,13 @@ class FieldsService {
439
442
  if (field.type === 'alias' || field.type === 'unknown')
440
443
  return;
441
444
  if ((_a = field.schema) === null || _a === void 0 ? void 0 : _a.has_auto_increment) {
442
- column = table.increments(field.field);
445
+ if (field.type === 'bigInteger') {
446
+ // Create an auto-incremented big integer (MySQL, PostgreSQL) or an auto-incremented integer (other DBs)
447
+ column = table.bigIncrements(field.field);
448
+ }
449
+ else {
450
+ column = table.increments(field.field);
451
+ }
443
452
  }
444
453
  else if (field.type === 'string') {
445
454
  column = table.string(field.field, (_c = (_b = field.schema) === null || _b === void 0 ? void 0 : _b.max_length) !== null && _c !== void 0 ? _c : undefined);
@@ -89,7 +89,16 @@ class GraphQLService {
89
89
  async execute({ document, variables, operationName, contextValue, }) {
90
90
  var _a;
91
91
  const schema = this.getSchema();
92
- const validationErrors = (0, graphql_1.validate)(schema, document, graphql_1.specifiedRules);
92
+ const validationErrors = (0, graphql_1.validate)(schema, document, [
93
+ ...graphql_1.specifiedRules,
94
+ (context) => ({
95
+ Field(node) {
96
+ if (env_1.default.GRAPHQL_INTROSPECTION === false && (node.name.value === '__schema' || node.name.value === '__type')) {
97
+ context.reportError(new graphql_1.GraphQLError('GraphQL introspection is not allowed. The query contained __schema or __type.', [node]));
98
+ }
99
+ },
100
+ }),
101
+ ]);
93
102
  if (validationErrors.length > 0) {
94
103
  throw new exceptions_1.GraphQLValidationException({ graphqlErrors: validationErrors });
95
104
  }
@@ -161,16 +170,7 @@ class GraphQLService {
161
170
  acc[collectionName] = ReadCollectionTypes[collection.collection].getResolver(collection.collection);
162
171
  if (this.schema.collections[collection.collection].singleton === false) {
163
172
  acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_by_id`);
164
- const hasAggregate = Object.values(collection.fields).some((field) => {
165
- const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
166
- if (graphqlType === graphql_1.GraphQLInt || graphqlType === graphql_1.GraphQLFloat) {
167
- return true;
168
- }
169
- return false;
170
- });
171
- if (hasAggregate) {
172
- acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_aggregated`);
173
- }
173
+ acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_aggregated`);
174
174
  }
175
175
  return acc;
176
176
  }, {}));
@@ -415,7 +415,8 @@ class GraphQLService {
415
415
  const { CollectionTypes: ReadCollectionTypes } = getTypes('read');
416
416
  const ReadableCollectionFilterTypes = {};
417
417
  const AggregatedFunctions = {};
418
- const AggregatedFilters = {};
418
+ const AggregatedFields = {};
419
+ const AggregateMethods = {};
419
420
  const StringFilterOperators = schemaComposer.createInputTC({
420
421
  name: 'string_filter_operators',
421
422
  fields: {
@@ -671,7 +672,7 @@ class GraphQLService {
671
672
  _and: [ReadableCollectionFilterTypes[collection.collection]],
672
673
  _or: [ReadableCollectionFilterTypes[collection.collection]],
673
674
  });
674
- AggregatedFilters[collection.collection] = schemaComposer.createObjectTC({
675
+ AggregatedFields[collection.collection] = schemaComposer.createObjectTC({
675
676
  name: `${collection.collection}_aggregated_fields`,
676
677
  fields: Object.values(collection.fields).reduce((acc, field) => {
677
678
  const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
@@ -689,46 +690,71 @@ class GraphQLService {
689
690
  return acc;
690
691
  }, {}),
691
692
  });
692
- AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({
693
- name: `${collection.collection}_aggregated`,
694
- fields: {
695
- group: {
696
- name: 'group',
697
- type: graphql_compose_1.GraphQLJSON,
698
- },
693
+ AggregateMethods[collection.collection] = {
694
+ group: {
695
+ name: 'group',
696
+ type: graphql_compose_1.GraphQLJSON,
697
+ },
698
+ countAll: {
699
+ name: 'countAll',
700
+ type: graphql_1.GraphQLInt,
701
+ },
702
+ count: {
703
+ name: 'count',
704
+ type: schemaComposer.createObjectTC({
705
+ name: `${collection.collection}_aggregated_count`,
706
+ fields: Object.values(collection.fields).reduce((acc, field) => {
707
+ acc[field.field] = {
708
+ type: graphql_1.GraphQLInt,
709
+ description: field.note,
710
+ };
711
+ return acc;
712
+ }, {}),
713
+ }),
714
+ },
715
+ };
716
+ const hasNumericAggregates = Object.values(collection.fields).some((field) => {
717
+ const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
718
+ if (graphqlType === graphql_1.GraphQLInt || graphqlType === graphql_1.GraphQLFloat) {
719
+ return true;
720
+ }
721
+ return false;
722
+ });
723
+ if (hasNumericAggregates) {
724
+ Object.assign(AggregateMethods[collection.collection], {
699
725
  avg: {
700
726
  name: 'avg',
701
- type: AggregatedFilters[collection.collection],
727
+ type: AggregatedFields[collection.collection],
702
728
  },
703
729
  sum: {
704
730
  name: 'sum',
705
- type: AggregatedFilters[collection.collection],
706
- },
707
- count: {
708
- name: 'count',
709
- type: AggregatedFilters[collection.collection],
731
+ type: AggregatedFields[collection.collection],
710
732
  },
711
733
  countDistinct: {
712
734
  name: 'countDistinct',
713
- type: AggregatedFilters[collection.collection],
735
+ type: AggregatedFields[collection.collection],
714
736
  },
715
737
  avgDistinct: {
716
738
  name: 'avgDistinct',
717
- type: AggregatedFilters[collection.collection],
739
+ type: AggregatedFields[collection.collection],
718
740
  },
719
741
  sumDistinct: {
720
742
  name: 'sumDistinct',
721
- type: AggregatedFilters[collection.collection],
743
+ type: AggregatedFields[collection.collection],
722
744
  },
723
745
  min: {
724
746
  name: 'min',
725
- type: AggregatedFilters[collection.collection],
747
+ type: AggregatedFields[collection.collection],
726
748
  },
727
749
  max: {
728
750
  name: 'max',
729
- type: AggregatedFilters[collection.collection],
751
+ type: AggregatedFields[collection.collection],
730
752
  },
731
- },
753
+ });
754
+ }
755
+ AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({
756
+ name: `${collection.collection}_aggregated`,
757
+ fields: AggregateMethods[collection.collection],
732
758
  });
733
759
  ReadCollectionTypes[collection.collection].addResolver({
734
760
  name: collection.collection,
@@ -1175,6 +1201,7 @@ class GraphQLService {
1175
1201
  continue;
1176
1202
  selection = selection;
1177
1203
  let current;
1204
+ let currentAlias = null;
1178
1205
  // Union type (Many-to-Any)
1179
1206
  if (selection.kind === 'InlineFragment') {
1180
1207
  if (selection.typeCondition.name.value.startsWith('__'))
@@ -1187,8 +1214,20 @@ class GraphQLService {
1187
1214
  if (selection.name.value.startsWith('__'))
1188
1215
  continue;
1189
1216
  current = selection.name.value;
1217
+ if (selection.alias) {
1218
+ currentAlias = selection.alias.value;
1219
+ }
1190
1220
  if (parent) {
1191
1221
  current = `${parent}.${current}`;
1222
+ if (currentAlias) {
1223
+ currentAlias = `${parent}.${currentAlias}`;
1224
+ // add nested aliases into deep query
1225
+ if (selection.selectionSet) {
1226
+ if (!query.deep)
1227
+ query.deep = {};
1228
+ (0, lodash_1.set)(query.deep, parent, (0, lodash_1.merge)((0, lodash_1.get)(query.deep, parent), { _alias: { [selection.alias.value]: selection.name.value } }));
1229
+ }
1230
+ }
1192
1231
  }
1193
1232
  }
1194
1233
  if (selection.selectionSet) {
@@ -1203,7 +1242,7 @@ class GraphQLService {
1203
1242
  }
1204
1243
  }
1205
1244
  else {
1206
- children = parseFields(selection.selectionSet.selections, current);
1245
+ children = parseFields(selection.selectionSet.selections, currentAlias !== null && currentAlias !== void 0 ? currentAlias : current);
1207
1246
  }
1208
1247
  fields.push(...children);
1209
1248
  }
@@ -1215,7 +1254,7 @@ class GraphQLService {
1215
1254
  if (!query.deep)
1216
1255
  query.deep = {};
1217
1256
  const args = this.parseArgs(selection.arguments, variableValues);
1218
- (0, lodash_1.set)(query.deep, current, (0, lodash_1.merge)((0, lodash_1.get)(query.deep, current), (0, lodash_1.mapKeys)((0, sanitize_query_1.sanitizeQuery)(args, this.accountability), (value, key) => `_${key}`)));
1257
+ (0, lodash_1.set)(query.deep, currentAlias !== null && currentAlias !== void 0 ? currentAlias : current, (0, lodash_1.merge)((0, lodash_1.get)(query.deep, currentAlias !== null && currentAlias !== void 0 ? currentAlias : current), (0, lodash_1.mapKeys)((0, sanitize_query_1.sanitizeQuery)(args, this.accountability), (value, key) => `_${key}`)));
1219
1258
  }
1220
1259
  }
1221
1260
  }
@@ -1274,8 +1313,10 @@ class GraphQLService {
1274
1313
  */
1275
1314
  formatError(error) {
1276
1315
  if (Array.isArray(error)) {
1316
+ error[0].extensions.code = error[0].code;
1277
1317
  return new graphql_1.GraphQLError(error[0].message, undefined, undefined, undefined, undefined, error[0]);
1278
1318
  }
1319
+ error.extensions.code = error.code;
1279
1320
  return new graphql_1.GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
1280
1321
  }
1281
1322
  /**
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
+ import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
2
3
  import { Knex } from 'knex';
3
4
  import { AbstractServiceOptions, File } from '../types';
4
- import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
5
5
  export declare class ImportService {
6
6
  knex: Knex;
7
7
  accountability: Accountability | null;
@@ -4,24 +4,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ExportService = exports.ImportService = void 0;
7
- const database_1 = __importDefault(require("../database"));
8
- const exceptions_1 = require("../exceptions");
9
- const StreamArray_1 = __importDefault(require("stream-json/streamers/StreamArray"));
10
- const items_1 = require("./items");
7
+ const utils_1 = require("@directus/shared/utils");
11
8
  const async_1 = require("async");
12
- const destroy_1 = __importDefault(require("destroy"));
13
9
  const csv_parser_1 = __importDefault(require("csv-parser"));
14
- const lodash_1 = require("lodash");
10
+ const destroy_1 = __importDefault(require("destroy"));
11
+ const fs_extra_1 = require("fs-extra");
15
12
  const js2xmlparser_1 = require("js2xmlparser");
16
13
  const json2csv_1 = require("json2csv");
17
- const fs_extra_1 = require("fs-extra");
14
+ const lodash_1 = require("lodash");
15
+ const StreamArray_1 = __importDefault(require("stream-json/streamers/StreamArray"));
16
+ const strip_bom_stream_1 = __importDefault(require("strip-bom-stream"));
18
17
  const tmp_promise_1 = require("tmp-promise");
18
+ const database_1 = __importDefault(require("../database"));
19
19
  const env_1 = __importDefault(require("../env"));
20
- const files_1 = require("./files");
20
+ const exceptions_1 = require("../exceptions");
21
+ const logger_1 = __importDefault(require("../logger"));
21
22
  const get_date_formatted_1 = require("../utils/get-date-formatted");
22
- const utils_1 = require("@directus/shared/utils");
23
+ const parse_json_1 = require("../utils/parse-json");
24
+ const files_1 = require("./files");
25
+ const items_1 = require("./items");
23
26
  const notifications_1 = require("./notifications");
24
- const logger_1 = __importDefault(require("../logger"));
25
27
  class ImportService {
26
28
  constructor(options) {
27
29
  this.knex = options.knex || (0, database_1.default)();
@@ -91,6 +93,7 @@ class ImportService {
91
93
  });
92
94
  return new Promise((resolve, reject) => {
93
95
  stream
96
+ .pipe((0, strip_bom_stream_1.default)())
94
97
  .pipe((0, csv_parser_1.default)())
95
98
  .on('data', (value) => {
96
99
  const obj = (0, lodash_1.transform)(value, (result, value, key) => {
@@ -99,7 +102,7 @@ class ImportService {
99
102
  }
100
103
  else {
101
104
  try {
102
- const parsedJson = JSON.parse(value);
105
+ const parsedJson = (0, parse_json_1.parseJSON)(value);
103
106
  (0, lodash_1.set)(result, key, parsedJson);
104
107
  }
105
108
  catch {
@@ -1,7 +1,7 @@
1
- import { Knex } from 'knex';
1
+ import { Accountability, PermissionsAction, Query, SchemaOverview } from '@directus/shared/types';
2
2
  import Keyv from 'keyv';
3
- import { Accountability, Query, PermissionsAction, SchemaOverview } from '@directus/shared/types';
4
- import { AbstractService, AbstractServiceOptions, Item as AnyItem, PrimaryKey, MutationOptions } from '../types';
3
+ import { Knex } from 'knex';
4
+ import { AbstractService, AbstractServiceOptions, Item as AnyItem, MutationOptions, PrimaryKey } from '../types';
5
5
  export declare type QueryOptions = {
6
6
  stripNonRequested?: boolean;
7
7
  permissionsAction?: PermissionsAction;
@@ -15,8 +15,8 @@ const translate_1 = require("../exceptions/database/translate");
15
15
  const types_1 = require("../types");
16
16
  const get_ast_from_query_1 = __importDefault(require("../utils/get-ast-from-query"));
17
17
  const authorization_1 = require("./authorization");
18
- const payload_1 = require("./payload");
19
18
  const index_1 = require("./index");
19
+ const payload_1 = require("./payload");
20
20
  class ItemsService {
21
21
  constructor(collection, options) {
22
22
  this.collection = collection;
@@ -90,8 +90,13 @@ class ItemsService {
90
90
  // In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
91
91
  let primaryKey = payloadWithTypeCasting[primaryKeyField];
92
92
  try {
93
- const result = await trx.insert(payloadWithoutAliases).into(this.collection).returning(primaryKeyField);
94
- primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : result[0];
93
+ const result = await trx
94
+ .insert(payloadWithoutAliases)
95
+ .into(this.collection)
96
+ .returning(primaryKeyField)
97
+ .then((result) => result[0]);
98
+ const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
99
+ primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : returnedKey;
95
100
  }
96
101
  catch (err) {
97
102
  throw await (0, translate_1.translateDatabaseError)(err);
@@ -255,6 +260,10 @@ class ItemsService {
255
260
  const primaryKeyField = this.schema.collections[this.collection].primary;
256
261
  const filterWithKey = { _and: [{ [primaryKeyField]: { _in: keys } }, (_a = query.filter) !== null && _a !== void 0 ? _a : {}] };
257
262
  const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
263
+ // Set query limit as the number of keys
264
+ if (Array.isArray(keys) && keys.length > 0 && !queryWithKey.limit) {
265
+ queryWithKey.limit = keys.length;
266
+ }
258
267
  const results = await this.readByQuery(queryWithKey, opts);
259
268
  return results;
260
269
  }
@@ -301,6 +310,8 @@ class ItemsService {
301
310
  accountability: this.accountability,
302
311
  })
303
312
  : payload;
313
+ // Sort keys to ensure that the order is maintained
314
+ keys.sort();
304
315
  if (this.accountability) {
305
316
  await authorizationService.checkAccess('update', this.collection, keys);
306
317
  }
@@ -354,13 +365,14 @@ class ItemsService {
354
365
  knex: trx,
355
366
  schema: this.schema,
356
367
  });
357
- const revisionIDs = await revisionsService.createMany(await Promise.all(activity.map(async (activity, index) => ({
368
+ const revisions = (await Promise.all(activity.map(async (activity, index) => ({
358
369
  activity: activity,
359
370
  collection: this.collection,
360
371
  item: keys[index],
361
372
  data: snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots[index]) : JSON.stringify(snapshots),
362
373
  delta: await payloadService.prepareDelta(payloadWithTypeCasting),
363
- }))));
374
+ })))).filter((revision) => revision.delta);
375
+ const revisionIDs = await revisionsService.createMany(revisions);
364
376
  for (let i = 0; i < revisionIDs.length; i++) {
365
377
  const revisionID = revisionIDs[i];
366
378
  if (opts === null || opts === void 0 ? void 0 : opts.onRevisionCreate) {
@@ -461,6 +473,7 @@ class ItemsService {
461
473
  const authorizationService = new authorization_1.AuthorizationService({
462
474
  accountability: this.accountability,
463
475
  schema: this.schema,
476
+ knex: this.knex,
464
477
  });
465
478
  await authorizationService.checkAccess('delete', this.collection, keys);
466
479
  }
@@ -529,7 +542,8 @@ class ItemsService {
529
542
  defaults[name] = null;
530
543
  continue;
531
544
  }
532
- defaults[name] = field.defaultValue;
545
+ if (field.defaultValue)
546
+ defaults[name] = field.defaultValue;
533
547
  }
534
548
  return defaults;
535
549
  }
@@ -1,7 +1,7 @@
1
- import { Knex } from 'knex';
2
- import { AbstractServiceOptions, Item, PrimaryKey } from '../types';
3
1
  import { Accountability, SchemaOverview } from '@directus/shared/types';
2
+ import { Knex } from 'knex';
4
3
  import { Helpers } from '../database/helpers';
4
+ import { AbstractServiceOptions, Item, PrimaryKey } from '../types';
5
5
  declare type Action = 'create' | 'read' | 'update';
6
6
  declare type Transformers = {
7
7
  [type: string]: (context: {
@@ -65,6 +65,6 @@ export declare class PayloadService {
65
65
  * Transforms the input partial payload to match the output structure, to have consistency
66
66
  * between delta and data
67
67
  */
68
- prepareDelta(data: Partial<Item>): Promise<string>;
68
+ prepareDelta(data: Partial<Item>): Promise<string | null>;
69
69
  }
70
70
  export {};
@@ -4,18 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PayloadService = void 0;
7
+ const utils_1 = require("@directus/shared/utils");
7
8
  const date_fns_1 = require("date-fns");
9
+ const flat_1 = require("flat");
8
10
  const joi_1 = __importDefault(require("joi"));
9
11
  const lodash_1 = require("lodash");
10
12
  const uuid_1 = require("uuid");
13
+ const wellknown_1 = require("wellknown");
11
14
  const database_1 = __importDefault(require("../database"));
12
- const exceptions_1 = require("../exceptions");
13
- const utils_1 = require("@directus/shared/utils");
14
- const items_1 = require("./items");
15
- const flat_1 = require("flat");
16
15
  const helpers_1 = require("../database/helpers");
17
- const wellknown_1 = require("wellknown");
16
+ const exceptions_1 = require("../exceptions");
18
17
  const generate_hash_1 = require("../utils/generate-hash");
18
+ const parse_json_1 = require("../utils/parse-json");
19
+ const items_1 = require("./items");
19
20
  /**
20
21
  * Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
21
22
  * handled correctly.
@@ -55,7 +56,7 @@ class PayloadService {
55
56
  if (action === 'read') {
56
57
  if (typeof value === 'string') {
57
58
  try {
58
- return JSON.parse(value);
59
+ return (0, parse_json_1.parseJSON)(value);
59
60
  }
60
61
  catch {
61
62
  return value;
@@ -196,7 +197,7 @@ class PayloadService {
196
197
  processGeometries(payloads, action) {
197
198
  const process = action == 'read'
198
199
  ? (value) => (typeof value === 'string' ? (0, wellknown_1.parse)(value) : value)
199
- : (value) => this.helpers.st.fromGeoJSON(typeof value == 'string' ? JSON.parse(value) : value);
200
+ : (value) => this.helpers.st.fromGeoJSON(typeof value == 'string' ? (0, parse_json_1.parseJSON)(value) : value);
200
201
  const fieldsInCollection = Object.entries(this.schema.collections[this.collection].fields);
201
202
  const geometryColumns = fieldsInCollection.filter(([_, field]) => field.type.startsWith('geometry'));
202
203
  for (const [name] of geometryColumns) {
@@ -318,7 +319,7 @@ class PayloadService {
318
319
  }
319
320
  const allowedCollections = relation.meta.one_allowed_collections;
320
321
  if (allowedCollections.includes(relatedCollection) === false) {
321
- throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}`);
322
+ throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}"`);
322
323
  }
323
324
  const itemsService = new items_1.ItemsService(relatedCollection, {
324
325
  accountability: this.accountability,
@@ -578,6 +579,8 @@ class PayloadService {
578
579
  }
579
580
  }
580
581
  payload = await this.processValues('read', payload);
582
+ if (Object.keys(payload).length === 0)
583
+ return null;
581
584
  return JSON.stringify(payload);
582
585
  }
583
586
  }
@@ -315,9 +315,7 @@ class OASSpecsService {
315
315
  schema: {
316
316
  properties: {
317
317
  data: {
318
- items: {
319
- $ref: `#/components/schemas/${tag.name}`,
320
- },
318
+ $ref: `#/components/schemas/${tag.name}`,
321
319
  },
322
320
  },
323
321
  },
@@ -18,6 +18,10 @@ export declare class UsersService extends ItemsService {
18
18
  */
19
19
  private checkPasswordPolicy;
20
20
  private checkRemainingAdminExistence;
21
+ /**
22
+ * Make sure there's at least one active admin user when updating user status
23
+ */
24
+ private checkRemainingActiveAdmin;
21
25
  /**
22
26
  * Create a new user
23
27
  */
@@ -101,6 +101,23 @@ class UsersService extends items_1.ItemsService {
101
101
  throw new exceptions_2.UnprocessableEntityException(`You can't remove the last admin user from the role.`);
102
102
  }
103
103
  }
104
+ /**
105
+ * Make sure there's at least one active admin user when updating user status
106
+ */
107
+ async checkRemainingActiveAdmin(excludeKeys) {
108
+ const otherAdminUsers = await this.knex
109
+ .count('*', { as: 'count' })
110
+ .from('directus_users')
111
+ .whereNotIn('directus_users.id', excludeKeys)
112
+ .andWhere({ 'directus_roles.admin_access': true })
113
+ .andWhere({ 'directus_users.status': 'active' })
114
+ .leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
115
+ .first();
116
+ const otherAdminUsersCount = +((otherAdminUsers === null || otherAdminUsers === void 0 ? void 0 : otherAdminUsers.count) || 0);
117
+ if (otherAdminUsersCount === 0) {
118
+ throw new exceptions_2.UnprocessableEntityException(`You can't change the active status of the last admin user.`);
119
+ }
120
+ }
104
121
  /**
105
122
  * Create a new user
106
123
  */
@@ -140,12 +157,18 @@ class UsersService extends items_1.ItemsService {
140
157
  * Update many users by primary key
141
158
  */
142
159
  async updateMany(keys, data, opts) {
160
+ var _a, _b;
143
161
  if (data.role) {
144
- const newRole = await this.knex.select('admin_access').from('directus_roles').where('id', data.role).first();
162
+ // data.role will be an object with id with GraphQL mutations
163
+ const roleId = (_b = (_a = data.role) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : data.role;
164
+ const newRole = await this.knex.select('admin_access').from('directus_roles').where('id', roleId).first();
145
165
  if (!(newRole === null || newRole === void 0 ? void 0 : newRole.admin_access)) {
146
166
  await this.checkRemainingAdminExistence(keys);
147
167
  }
148
168
  }
169
+ if (data.status !== undefined && data.status !== 'active') {
170
+ await this.checkRemainingActiveAdmin(keys);
171
+ }
149
172
  if (data.email) {
150
173
  if (keys.length > 1) {
151
174
  throw new record_not_unique_1.RecordNotUniqueException('email', {