directus 9.9.1 → 9.11.1

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 (67) 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/database/helpers/date/dialects/sqlite.js +6 -2
  10. package/dist/database/index.js +5 -0
  11. package/dist/database/migrations/20210225A-add-relations-sort-field.js +2 -1
  12. package/dist/database/migrations/20210506A-rename-interfaces.js +2 -1
  13. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  14. package/dist/database/migrations/20210805A-update-groups.js +2 -1
  15. package/dist/database/migrations/20210805B-change-image-metadata-structure.js +3 -2
  16. package/dist/database/migrations/20211007A-update-presets.js +5 -4
  17. package/dist/database/run-ast.js +10 -14
  18. package/dist/database/system-data/fields/activity.yaml +3 -0
  19. package/dist/database/system-data/fields/dashboards.yaml +3 -1
  20. package/dist/database/system-data/fields/notifications.yaml +3 -1
  21. package/dist/database/system-data/fields/panels.yaml +3 -1
  22. package/dist/database/system-data/fields/shares.yaml +3 -1
  23. package/dist/env.js +193 -10
  24. package/dist/exceptions/index.d.ts +1 -0
  25. package/dist/exceptions/index.js +1 -0
  26. package/dist/exceptions/invalid-provider.d.ts +4 -0
  27. package/dist/exceptions/invalid-provider.js +10 -0
  28. package/dist/exceptions/range-not-satisfiable.d.ts +2 -2
  29. package/dist/exceptions/range-not-satisfiable.js +5 -1
  30. package/dist/middleware/graphql.js +2 -1
  31. package/dist/services/assets.js +27 -1
  32. package/dist/services/authentication.js +4 -1
  33. package/dist/services/fields.js +15 -8
  34. package/dist/services/graphql.js +61 -33
  35. package/dist/services/import-export.d.ts +1 -1
  36. package/dist/services/import-export.js +14 -11
  37. package/dist/services/items.d.ts +3 -3
  38. package/dist/services/items.js +31 -3
  39. package/dist/services/payload.d.ts +2 -2
  40. package/dist/services/payload.js +9 -8
  41. package/dist/services/users.d.ts +4 -0
  42. package/dist/services/users.js +20 -0
  43. package/dist/utils/apply-query.d.ts +2 -1
  44. package/dist/utils/apply-query.js +144 -149
  45. package/dist/utils/apply-snapshot.d.ts +3 -3
  46. package/dist/utils/apply-snapshot.js +64 -49
  47. package/dist/utils/get-ast-from-query.js +1 -7
  48. package/dist/utils/get-column-path.d.ts +16 -0
  49. package/dist/utils/get-column-path.js +46 -0
  50. package/dist/utils/get-default-value.js +4 -3
  51. package/dist/utils/get-permissions.d.ts +1 -1
  52. package/dist/utils/get-permissions.js +9 -8
  53. package/dist/utils/get-relation-info.d.ts +7 -0
  54. package/dist/utils/get-relation-info.js +45 -0
  55. package/dist/utils/get-relation-type.d.ts +1 -1
  56. package/dist/utils/get-schema.js +2 -1
  57. package/dist/utils/get-snapshot.js +22 -4
  58. package/dist/utils/merge-permissions-for-share.js +1 -1
  59. package/dist/utils/parse-json.d.ts +5 -0
  60. package/dist/utils/parse-json.js +19 -0
  61. package/dist/utils/reduce-schema.js +4 -5
  62. package/dist/utils/sanitize-query.d.ts +1 -2
  63. package/dist/utils/sanitize-query.js +6 -5
  64. package/dist/utils/validate-keys.d.ts +6 -0
  65. package/dist/utils/validate-keys.js +28 -0
  66. package/dist/utils/validate-query.js +1 -1
  67. package/package.json +16 -18
@@ -15,8 +15,9 @@ 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
+ const validate_keys_1 = require("../utils/validate-keys");
20
21
  class ItemsService {
21
22
  constructor(collection, options) {
22
23
  this.collection = collection;
@@ -90,8 +91,13 @@ class ItemsService {
90
91
  // In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
91
92
  let primaryKey = payloadWithTypeCasting[primaryKeyField];
92
93
  try {
93
- const result = await trx.insert(payloadWithoutAliases).into(this.collection).returning(primaryKeyField);
94
- primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : result[0];
94
+ const result = await trx
95
+ .insert(payloadWithoutAliases)
96
+ .into(this.collection)
97
+ .returning(primaryKeyField)
98
+ .then((result) => result[0]);
99
+ const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
100
+ primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : returnedKey;
95
101
  }
96
102
  catch (err) {
97
103
  throw await (0, translate_1.translateDatabaseError)(err);
@@ -239,6 +245,7 @@ class ItemsService {
239
245
  */
240
246
  async readOne(key, query = {}, opts) {
241
247
  const primaryKeyField = this.schema.collections[this.collection].primary;
248
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
242
249
  const filterWithKey = (0, lodash_1.assign)({}, query.filter, { [primaryKeyField]: { _eq: key } });
243
250
  const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
244
251
  const results = await this.readByQuery(queryWithKey, opts);
@@ -253,8 +260,13 @@ class ItemsService {
253
260
  async readMany(keys, query = {}, opts) {
254
261
  var _a;
255
262
  const primaryKeyField = this.schema.collections[this.collection].primary;
263
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
256
264
  const filterWithKey = { _and: [{ [primaryKeyField]: { _in: keys } }, (_a = query.filter) !== null && _a !== void 0 ? _a : {}] };
257
265
  const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
266
+ // Set query limit as the number of keys
267
+ if (Array.isArray(keys) && keys.length > 0 && !queryWithKey.limit) {
268
+ queryWithKey.limit = keys.length;
269
+ }
258
270
  const results = await this.readByQuery(queryWithKey, opts);
259
271
  return results;
260
272
  }
@@ -263,12 +275,16 @@ class ItemsService {
263
275
  */
264
276
  async updateByQuery(query, data, opts) {
265
277
  const keys = await this.getKeysByQuery(query);
278
+ const primaryKeyField = this.schema.collections[this.collection].primary;
279
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
266
280
  return keys.length ? await this.updateMany(keys, data, opts) : [];
267
281
  }
268
282
  /**
269
283
  * Update a single item by primary key
270
284
  */
271
285
  async updateOne(key, data, opts) {
286
+ const primaryKeyField = this.schema.collections[this.collection].primary;
287
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
272
288
  await this.updateMany([key], data, opts);
273
289
  return key;
274
290
  }
@@ -277,6 +293,7 @@ class ItemsService {
277
293
  */
278
294
  async updateMany(keys, data, opts) {
279
295
  const primaryKeyField = this.schema.collections[this.collection].primary;
296
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
280
297
  const fields = Object.keys(this.schema.collections[this.collection].fields);
281
298
  const aliases = Object.values(this.schema.collections[this.collection].fields)
282
299
  .filter((field) => field.alias === true)
@@ -301,6 +318,8 @@ class ItemsService {
301
318
  accountability: this.accountability,
302
319
  })
303
320
  : payload;
321
+ // Sort keys to ensure that the order is maintained
322
+ keys.sort();
304
323
  if (this.accountability) {
305
324
  await authorizationService.checkAccess('update', this.collection, keys);
306
325
  }
@@ -404,6 +423,9 @@ class ItemsService {
404
423
  async upsertOne(payload, opts) {
405
424
  const primaryKeyField = this.schema.collections[this.collection].primary;
406
425
  const primaryKey = payload[primaryKeyField];
426
+ if (primaryKey) {
427
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, primaryKey);
428
+ }
407
429
  const exists = primaryKey &&
408
430
  !!(await this.knex
409
431
  .select(primaryKeyField)
@@ -444,12 +466,16 @@ class ItemsService {
444
466
  */
445
467
  async deleteByQuery(query, opts) {
446
468
  const keys = await this.getKeysByQuery(query);
469
+ const primaryKeyField = this.schema.collections[this.collection].primary;
470
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
447
471
  return keys.length ? await this.deleteMany(keys, opts) : [];
448
472
  }
449
473
  /**
450
474
  * Delete a single item by primary key
451
475
  */
452
476
  async deleteOne(key, opts) {
477
+ const primaryKeyField = this.schema.collections[this.collection].primary;
478
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
453
479
  await this.deleteMany([key], opts);
454
480
  return key;
455
481
  }
@@ -458,10 +484,12 @@ class ItemsService {
458
484
  */
459
485
  async deleteMany(keys, opts) {
460
486
  const primaryKeyField = this.schema.collections[this.collection].primary;
487
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
461
488
  if (this.accountability && this.accountability.admin !== true) {
462
489
  const authorizationService = new authorization_1.AuthorizationService({
463
490
  accountability: this.accountability,
464
491
  schema: this.schema,
492
+ knex: this.knex,
465
493
  });
466
494
  await authorizationService.checkAccess('delete', this.collection, keys);
467
495
  }
@@ -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: {
@@ -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,
@@ -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
  */
@@ -149,6 +166,9 @@ class UsersService extends items_1.ItemsService {
149
166
  await this.checkRemainingAdminExistence(keys);
150
167
  }
151
168
  }
169
+ if (data.status !== undefined && data.status !== 'active') {
170
+ await this.checkRemainingActiveAdmin(keys);
171
+ }
152
172
  if (data.email) {
153
173
  if (keys.length > 1) {
154
174
  throw new record_not_unique_1.RecordNotUniqueException('email', {
@@ -1,9 +1,10 @@
1
- import { Knex } from 'knex';
2
1
  import { Aggregate, Filter, Query, SchemaOverview } from '@directus/shared/types';
2
+ import { Knex } from 'knex';
3
3
  /**
4
4
  * Apply the Query to a given Knex query builder instance
5
5
  */
6
6
  export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, subQuery?: boolean): Knex.QueryBuilder;
7
+ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootSort: string[], collection: string, subQuery?: boolean): void;
7
8
  export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, subQuery?: boolean): Knex.QueryBuilder<any, any>;
8
9
  export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
9
10
  export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
@@ -3,14 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.applyAggregate = exports.applySearch = exports.applyFilter = void 0;
6
+ exports.applyAggregate = exports.applySearch = exports.applyFilter = exports.applySort = void 0;
7
7
  const lodash_1 = require("lodash");
8
8
  const nanoid_1 = require("nanoid");
9
9
  const uuid_validate_1 = __importDefault(require("uuid-validate"));
10
- const exceptions_1 = require("../exceptions");
11
- const get_column_1 = require("./get-column");
12
- const get_relation_type_1 = require("./get-relation-type");
13
10
  const helpers_1 = require("../database/helpers");
11
+ const invalid_query_1 = require("../exceptions/invalid-query");
12
+ const get_column_1 = require("./get-column");
13
+ const get_column_path_1 = require("./get-column-path");
14
+ const get_relation_info_1 = require("./get-relation-info");
14
15
  const utils_1 = require("@directus/shared/utils");
15
16
  const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz', 5);
16
17
  /**
@@ -18,18 +19,7 @@ const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz',
18
19
  */
19
20
  function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false) {
20
21
  if (query.sort) {
21
- dbQuery.orderBy(query.sort.map((sortField) => {
22
- let column = sortField;
23
- let order = 'asc';
24
- if (sortField.startsWith('-')) {
25
- column = column.substring(1);
26
- order = 'desc';
27
- }
28
- return {
29
- order,
30
- column: (0, get_column_1.getColumn)(knex, collection, column, false, schema),
31
- };
32
- }));
22
+ applySort(knex, schema, dbQuery, query.sort, collection, subQuery);
33
23
  }
34
24
  if (typeof query.limit === 'number' && query.limit !== -1) {
35
25
  dbQuery.limit(query.limit);
@@ -55,44 +45,109 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
55
45
  return dbQuery;
56
46
  }
57
47
  exports.default = applyQuery;
58
- function getRelationInfo(relations, collection, field) {
59
- var _a, _b;
60
- const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
61
- if (implicitRelation) {
62
- if (implicitRelation[2] === undefined) {
63
- const [m2oCollection, m2oField] = implicitRelation;
64
- const relation = {
65
- collection: m2oCollection,
66
- field: m2oField,
67
- related_collection: collection,
68
- schema: null,
69
- meta: null,
70
- };
71
- return { relation, relationType: 'o2m' };
48
+ function addJoin({ path, collection, aliasMap, rootQuery, subQuery, schema, relations, knex }) {
49
+ path = (0, lodash_1.clone)(path);
50
+ followRelation(path);
51
+ function followRelation(pathParts, parentCollection = collection, parentAlias) {
52
+ /**
53
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
54
+ */
55
+ const pathRoot = pathParts[0].split(':')[0];
56
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, parentCollection, pathRoot);
57
+ if (!relation) {
58
+ return;
72
59
  }
73
- else {
74
- const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
75
- const relation = {
76
- collection: a2oCollection,
77
- field: a2oItemField,
78
- related_collection: collection,
79
- schema: null,
80
- meta: {
81
- one_collection_field: a2oCollectionField,
82
- one_field: field,
83
- },
84
- };
85
- return { relation, relationType: 'o2a' };
60
+ const alias = generateAlias();
61
+ (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
62
+ if (relationType === 'm2o') {
63
+ rootQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
64
+ }
65
+ if (relationType === 'a2o') {
66
+ const pathScope = pathParts[0].split(':')[1];
67
+ if (!pathScope) {
68
+ throw new invalid_query_1.InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
69
+ }
70
+ rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
71
+ joinClause
72
+ .onVal(relation.meta.one_collection_field, '=', pathScope)
73
+ .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
74
+ });
75
+ }
76
+ if (relationType === 'o2a') {
77
+ rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
78
+ joinClause
79
+ .onVal(relation.meta.one_collection_field, '=', parentCollection)
80
+ .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
81
+ });
82
+ }
83
+ // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
84
+ if (relationType === 'o2m' && (subQuery === true || parentAlias !== undefined)) {
85
+ rootQuery.leftJoin({ [alias]: relation.collection }, `${parentAlias || parentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
86
+ }
87
+ if (relationType === 'm2o' || subQuery === true || (relationType === 'o2m' && parentAlias !== undefined)) {
88
+ let parent;
89
+ if (relationType === 'm2o') {
90
+ parent = relation.related_collection;
91
+ }
92
+ else if (relationType === 'a2o') {
93
+ const pathScope = pathParts[0].split(':')[1];
94
+ if (!pathScope) {
95
+ throw new invalid_query_1.InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
96
+ }
97
+ parent = pathScope;
98
+ }
99
+ else {
100
+ parent = relation.collection;
101
+ }
102
+ pathParts.shift();
103
+ if (pathParts.length) {
104
+ followRelation(pathParts, parent, alias);
105
+ }
86
106
  }
87
107
  }
88
- const relation = (_b = relations.find((relation) => {
89
- var _a;
90
- return ((relation.collection === collection && relation.field === field) ||
91
- (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
92
- })) !== null && _b !== void 0 ? _b : null;
93
- const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
94
- return { relation, relationType };
95
108
  }
109
+ function applySort(knex, schema, rootQuery, rootSort, collection, subQuery = false) {
110
+ const relations = schema.relations;
111
+ const aliasMap = {};
112
+ rootQuery.orderBy(rootSort.map((sortField) => {
113
+ const column = sortField.split('.');
114
+ let order = 'asc';
115
+ if (column.length > 1) {
116
+ if (sortField.startsWith('-')) {
117
+ order = 'desc';
118
+ }
119
+ if (column[0].startsWith('-')) {
120
+ column[0] = column[0].substring(1);
121
+ }
122
+ addJoin({
123
+ path: column,
124
+ collection,
125
+ aliasMap,
126
+ rootQuery,
127
+ subQuery,
128
+ schema,
129
+ relations,
130
+ knex,
131
+ });
132
+ const colPath = (0, get_column_path_1.getColumnPath)({ path: column, collection, aliasMap, relations }) || '';
133
+ const [alias, field] = colPath.split('.');
134
+ return {
135
+ order,
136
+ column: (0, get_column_1.getColumn)(knex, alias, field, false, schema),
137
+ };
138
+ }
139
+ let col = column[0];
140
+ if (sortField.startsWith('-')) {
141
+ col = column[0].substring(1);
142
+ order = 'desc';
143
+ }
144
+ return {
145
+ order,
146
+ column: (0, get_column_1.getColumn)(knex, collection, col, false, schema),
147
+ };
148
+ }));
149
+ }
150
+ exports.applySort = applySort;
96
151
  function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
97
152
  const helpers = (0, helpers_1.getHelpers)(knex);
98
153
  const relations = schema.relations;
@@ -114,68 +169,16 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
114
169
  }
115
170
  const filterPath = getFilterPath(key, value);
116
171
  if (filterPath.length > 1) {
117
- addJoin(filterPath, collection);
118
- }
119
- }
120
- function addJoin(path, collection) {
121
- path = (0, lodash_1.clone)(path);
122
- followRelation(path);
123
- function followRelation(pathParts, parentCollection = collection, parentAlias) {
124
- /**
125
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
126
- */
127
- const pathRoot = pathParts[0].split(':')[0];
128
- const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
129
- if (!relation) {
130
- return;
131
- }
132
- const alias = generateAlias();
133
- (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
134
- if (relationType === 'm2o') {
135
- dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
136
- }
137
- if (relationType === 'a2o') {
138
- const pathScope = pathParts[0].split(':')[1];
139
- if (!pathScope) {
140
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
141
- }
142
- dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
143
- joinClause
144
- .onVal(relation.meta.one_collection_field, '=', pathScope)
145
- .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
146
- });
147
- }
148
- if (relationType === 'o2a') {
149
- dbQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
150
- joinClause
151
- .onVal(relation.meta.one_collection_field, '=', parentCollection)
152
- .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
153
- });
154
- }
155
- // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
156
- if (relationType === 'o2m' && (subQuery === true || parentAlias !== undefined)) {
157
- dbQuery.leftJoin({ [alias]: relation.collection }, `${parentAlias || parentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
158
- }
159
- if (relationType === 'm2o' || subQuery === true || (relationType === 'o2m' && parentAlias !== undefined)) {
160
- let parent;
161
- if (relationType === 'm2o') {
162
- parent = relation.related_collection;
163
- }
164
- else if (relationType === 'a2o') {
165
- const pathScope = pathParts[0].split(':')[1];
166
- if (!pathScope) {
167
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
168
- }
169
- parent = pathScope;
170
- }
171
- else {
172
- parent = relation.collection;
173
- }
174
- pathParts.shift();
175
- if (pathParts.length) {
176
- followRelation(pathParts, parent, alias);
177
- }
178
- }
172
+ addJoin({
173
+ path: filterPath,
174
+ collection,
175
+ knex,
176
+ schema,
177
+ relations,
178
+ subQuery,
179
+ rootQuery,
180
+ aliasMap,
181
+ });
179
182
  }
180
183
  }
181
184
  }
@@ -201,11 +204,11 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
201
204
  * For A2M fields, the path can contain an optional collection scope <field>:<scope>
202
205
  */
203
206
  const pathRoot = filterPath[0].split(':')[0];
204
- const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
207
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, collection, pathRoot);
205
208
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
206
209
  if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
207
210
  if (filterPath.length > 1) {
208
- const columnName = getWhereColumn(filterPath, collection);
211
+ const columnName = (0, get_column_path_1.getColumnPath)({ path: filterPath, collection, relations, aliasMap });
209
212
  if (!columnName)
210
213
  continue;
211
214
  applyFilterToQuery(columnName, filterOperator, filterValue, logical);
@@ -311,24 +314,48 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
311
314
  if (operator === '_neq') {
312
315
  dbQuery[logical].whereNot(selectionRaw, compareValue);
313
316
  }
317
+ if (operator === '_ieq') {
318
+ dbQuery[logical].whereRaw(`LOWER(??) = ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
319
+ }
320
+ if (operator === '_nieq') {
321
+ dbQuery[logical].whereRaw(`LOWER(??) <> ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
322
+ }
314
323
  if (operator === '_contains') {
315
324
  dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
316
325
  }
317
326
  if (operator === '_ncontains') {
318
327
  dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
319
328
  }
329
+ if (operator === '_icontains') {
330
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
331
+ }
332
+ if (operator === '_nicontains') {
333
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
334
+ }
320
335
  if (operator === '_starts_with') {
321
336
  dbQuery[logical].where(key, 'like', `${compareValue}%`);
322
337
  }
323
338
  if (operator === '_nstarts_with') {
324
339
  dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
325
340
  }
341
+ if (operator === '_istarts_with') {
342
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
343
+ }
344
+ if (operator === '_nistarts_with') {
345
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
346
+ }
326
347
  if (operator === '_ends_with') {
327
348
  dbQuery[logical].where(key, 'like', `%${compareValue}`);
328
349
  }
329
350
  if (operator === '_nends_with') {
330
351
  dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
331
352
  }
353
+ if (operator === '_iends_with') {
354
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
355
+ }
356
+ if (operator === '_niends_with') {
357
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
358
+ }
332
359
  if (operator === '_gt') {
333
360
  dbQuery[logical].where(selectionRaw, '>', compareValue);
334
361
  }
@@ -382,41 +409,6 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
382
409
  dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
383
410
  }
384
411
  }
385
- function getWhereColumn(path, collection) {
386
- return followRelation(path);
387
- function followRelation(pathParts, parentCollection = collection, parentAlias) {
388
- /**
389
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
390
- */
391
- const pathRoot = pathParts[0].split(':')[0];
392
- const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
393
- if (!relation) {
394
- throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
395
- }
396
- const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
397
- const remainingParts = pathParts.slice(1);
398
- let parent;
399
- if (relationType === 'a2o') {
400
- const pathScope = pathParts[0].split(':')[1];
401
- if (!pathScope) {
402
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
403
- }
404
- parent = pathScope;
405
- }
406
- else if (relationType === 'm2o') {
407
- parent = relation.related_collection;
408
- }
409
- else {
410
- parent = relation.collection;
411
- }
412
- if (remainingParts.length === 1) {
413
- return `${alias || parent}.${remainingParts[0]}`;
414
- }
415
- if (remainingParts.length) {
416
- return followRelation(remainingParts, parent, alias);
417
- }
418
- }
419
- }
420
412
  }
421
413
  }
422
414
  exports.applyFilter = applyFilter;
@@ -450,6 +442,9 @@ function applyAggregate(dbQuery, aggregate, collection) {
450
442
  if (operation === 'avgDistinct') {
451
443
  dbQuery.avgDistinct(`${collection}.${field}`, { as: `avgDistinct->${field}` });
452
444
  }
445
+ if (operation === 'countAll') {
446
+ dbQuery.count('*', { as: 'countAll' });
447
+ }
453
448
  if (operation === 'count') {
454
449
  if (field === '*') {
455
450
  dbQuery.count('*', { as: 'count' });
@@ -1,7 +1,7 @@
1
- import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
2
- import { Knex } from 'knex';
3
- import { Diff } from 'deep-diff';
4
1
  import { SchemaOverview } from '@directus/shared/types';
2
+ import { Diff } from 'deep-diff';
3
+ import { Knex } from 'knex';
4
+ import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
5
5
  export declare function applySnapshot(snapshot: Snapshot, options?: {
6
6
  database?: Knex;
7
7
  schema?: SchemaOverview;