directus 9.11.0 → 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 (82) hide show
  1. package/dist/database/index.js +5 -0
  2. package/dist/database/system-data/fields/activity.yaml +3 -0
  3. package/dist/database/system-data/fields/dashboards.yaml +3 -1
  4. package/dist/database/system-data/fields/notifications.yaml +3 -1
  5. package/dist/database/system-data/fields/panels.yaml +3 -1
  6. package/dist/database/system-data/fields/shares.yaml +3 -1
  7. package/dist/env.js +188 -10
  8. package/dist/services/items.js +16 -0
  9. package/dist/utils/{apply-query/index.d.ts → apply-query.d.ts} +0 -0
  10. package/dist/utils/{apply-query/index.js → apply-query.js} +147 -48
  11. package/dist/utils/validate-keys.d.ts +6 -0
  12. package/dist/utils/validate-keys.js +28 -0
  13. package/dist/utils/validate-query.js +1 -1
  14. package/package.json +12 -12
  15. package/dist/utils/apply-query/operators/between.operator.d.ts +0 -2
  16. package/dist/utils/apply-query/operators/between.operator.js +0 -16
  17. package/dist/utils/apply-query/operators/contains.operator.d.ts +0 -2
  18. package/dist/utils/apply-query/operators/contains.operator.js +0 -9
  19. package/dist/utils/apply-query/operators/ends-with.operator.d.ts +0 -2
  20. package/dist/utils/apply-query/operators/ends-with.operator.js +0 -9
  21. package/dist/utils/apply-query/operators/equals.operator.d.ts +0 -2
  22. package/dist/utils/apply-query/operators/equals.operator.js +0 -9
  23. package/dist/utils/apply-query/operators/greather-than-equals.operator.d.ts +0 -2
  24. package/dist/utils/apply-query/operators/greather-than-equals.operator.js +0 -9
  25. package/dist/utils/apply-query/operators/greather-than.operator.d.ts +0 -2
  26. package/dist/utils/apply-query/operators/greather-than.operator.js +0 -9
  27. package/dist/utils/apply-query/operators/in.operator.d.ts +0 -2
  28. package/dist/utils/apply-query/operators/in.operator.js +0 -14
  29. package/dist/utils/apply-query/operators/index.d.ts +0 -3
  30. package/dist/utils/apply-query/operators/index.js +0 -72
  31. package/dist/utils/apply-query/operators/insensitive-contains.operator.d.ts +0 -2
  32. package/dist/utils/apply-query/operators/insensitive-contains.operator.js +0 -9
  33. package/dist/utils/apply-query/operators/insensitive-ends-with.operator.d.ts +0 -2
  34. package/dist/utils/apply-query/operators/insensitive-ends-with.operator.js +0 -9
  35. package/dist/utils/apply-query/operators/insensitive-equals.operator.d.ts +0 -2
  36. package/dist/utils/apply-query/operators/insensitive-equals.operator.js +0 -9
  37. package/dist/utils/apply-query/operators/insensitive-not-contains.operator.d.ts +0 -2
  38. package/dist/utils/apply-query/operators/insensitive-not-contains.operator.js +0 -9
  39. package/dist/utils/apply-query/operators/insensitive-not-ends-with.operator.d.ts +0 -2
  40. package/dist/utils/apply-query/operators/insensitive-not-ends-with.operator.js +0 -9
  41. package/dist/utils/apply-query/operators/insensitive-not-equals.operator.d.ts +0 -2
  42. package/dist/utils/apply-query/operators/insensitive-not-equals.operator.js +0 -9
  43. package/dist/utils/apply-query/operators/insensitive-not-starts-with.operator.d.ts +0 -2
  44. package/dist/utils/apply-query/operators/insensitive-not-starts-with.operator.js +0 -9
  45. package/dist/utils/apply-query/operators/insensitive-starts-with.operator.d.ts +0 -2
  46. package/dist/utils/apply-query/operators/insensitive-starts-with.operator.js +0 -9
  47. package/dist/utils/apply-query/operators/intersects-bbox.operator.d.ts +0 -2
  48. package/dist/utils/apply-query/operators/intersects-bbox.operator.js +0 -9
  49. package/dist/utils/apply-query/operators/intersects.operator.d.ts +0 -2
  50. package/dist/utils/apply-query/operators/intersects.operator.js +0 -9
  51. package/dist/utils/apply-query/operators/is-empty.operator.d.ts +0 -2
  52. package/dist/utils/apply-query/operators/is-empty.operator.js +0 -14
  53. package/dist/utils/apply-query/operators/is-not-empty.operator.d.ts +0 -2
  54. package/dist/utils/apply-query/operators/is-not-empty.operator.js +0 -14
  55. package/dist/utils/apply-query/operators/is-not-null.operator.d.ts +0 -2
  56. package/dist/utils/apply-query/operators/is-not-null.operator.js +0 -14
  57. package/dist/utils/apply-query/operators/is-null.operator.d.ts +0 -2
  58. package/dist/utils/apply-query/operators/is-null.operator.js +0 -14
  59. package/dist/utils/apply-query/operators/less-than-equals.operator.d.ts +0 -2
  60. package/dist/utils/apply-query/operators/less-than-equals.operator.js +0 -9
  61. package/dist/utils/apply-query/operators/less-than.operator.d.ts +0 -2
  62. package/dist/utils/apply-query/operators/less-than.operator.js +0 -9
  63. package/dist/utils/apply-query/operators/not-between.operator.d.ts +0 -2
  64. package/dist/utils/apply-query/operators/not-between.operator.js +0 -16
  65. package/dist/utils/apply-query/operators/not-contains.operator.d.ts +0 -2
  66. package/dist/utils/apply-query/operators/not-contains.operator.js +0 -9
  67. package/dist/utils/apply-query/operators/not-ends-with.operator.d.ts +0 -2
  68. package/dist/utils/apply-query/operators/not-ends-with.operator.js +0 -9
  69. package/dist/utils/apply-query/operators/not-equals.operator.d.ts +0 -2
  70. package/dist/utils/apply-query/operators/not-equals.operator.js +0 -9
  71. package/dist/utils/apply-query/operators/not-in.operator.d.ts +0 -2
  72. package/dist/utils/apply-query/operators/not-in.operator.js +0 -14
  73. package/dist/utils/apply-query/operators/not-intersects-bbox.operator.d.ts +0 -2
  74. package/dist/utils/apply-query/operators/not-intersects-bbox.operator.js +0 -9
  75. package/dist/utils/apply-query/operators/not-intersects.operator.d.ts +0 -2
  76. package/dist/utils/apply-query/operators/not-intersects.operator.js +0 -9
  77. package/dist/utils/apply-query/operators/not-starts-with.operator.d.ts +0 -2
  78. package/dist/utils/apply-query/operators/not-starts-with.operator.js +0 -9
  79. package/dist/utils/apply-query/operators/operator-register.d.ts +0 -13
  80. package/dist/utils/apply-query/operators/operator-register.js +0 -7
  81. package/dist/utils/apply-query/operators/starts-with.operator.d.ts +0 -2
  82. package/dist/utils/apply-query/operators/starts-with.operator.js +0 -9
@@ -54,6 +54,11 @@ function getDatabase() {
54
54
  requiredEnvVars.push('DB_CONNECTION_STRING');
55
55
  }
56
56
  break;
57
+ case 'mssql':
58
+ if (!env_1.default.DB_TYPE || env_1.default.DB_TYPE === 'default') {
59
+ requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
60
+ }
61
+ break;
57
62
  default:
58
63
  requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
59
64
  }
@@ -37,6 +37,9 @@ fields:
37
37
 
38
38
  - field: timestamp
39
39
  display: datetime
40
+ special:
41
+ - date-created
42
+ - cast-timestamp
40
43
  options:
41
44
  relative: true
42
45
  width: half
@@ -8,7 +8,9 @@ fields:
8
8
  - field: panels
9
9
  special: o2m
10
10
  - field: date_created
11
- special: date-created
11
+ special:
12
+ - date-created
13
+ - cast-timestamp
12
14
  - field: user_created
13
15
  special: user-created
14
16
  - field: note
@@ -3,7 +3,9 @@ table: directus_notifications
3
3
  fields:
4
4
  - field: id
5
5
  - field: timestamp
6
- special: date-created
6
+ special:
7
+ - date-created
8
+ - cast-timestamp
7
9
  - field: status
8
10
  - field: recipient
9
11
  - field: sender
@@ -17,7 +17,9 @@ fields:
17
17
  - field: options
18
18
  special: cast-json
19
19
  - field: date_created
20
- special: date-created
20
+ special:
21
+ - date-created
22
+ - cast-timestamp
21
23
  - field: user_created
22
24
  special: user-created
23
25
  - field: dashboard
@@ -51,7 +51,9 @@ fields:
51
51
  readonly: true
52
52
 
53
53
  - field: date_created
54
- special: date-created
54
+ special:
55
+ - date-created
56
+ - cast-timestamp
55
57
  width: half
56
58
  readonly: true
57
59
  conditions:
package/dist/env.js CHANGED
@@ -15,6 +15,179 @@ const path_1 = __importDefault(require("path"));
15
15
  const require_yaml_1 = require("./utils/require-yaml");
16
16
  const utils_1 = require("@directus/shared/utils");
17
17
  const parse_json_1 = require("./utils/parse-json");
18
+ // keeping this here for now to prevent a circular import to constants.ts
19
+ const allowedEnvironmentVars = [
20
+ // general
21
+ 'CONFIG_PATH',
22
+ 'HOST',
23
+ 'PORT',
24
+ 'PUBLIC_URL',
25
+ 'LOG_LEVEL',
26
+ 'LOG_STYLE',
27
+ 'MAX_PAYLOAD_SIZE',
28
+ 'ROOT_REDIRECT',
29
+ 'SERVE_APP',
30
+ 'GRAPHQL_INTROSPECTION',
31
+ // server
32
+ 'SERVER_KEEP_ALIVE_TIMEOUT',
33
+ 'SERVER_HEADERS_TIMEOUT',
34
+ // database
35
+ 'DB_CLIENT',
36
+ 'DB_HOST',
37
+ 'DB_PORT',
38
+ 'DB_DATABASE',
39
+ 'DB_USER',
40
+ 'DB_PASSWORD',
41
+ 'DB_FILENAME',
42
+ 'DB_CONNECTION_STRING',
43
+ 'DB_POOL_.+',
44
+ 'DB_EXCLUDE_TABLES',
45
+ 'DB_CHARSET',
46
+ 'DB_VERSION',
47
+ // security
48
+ 'KEY',
49
+ 'SECRET',
50
+ 'ACCESS_TOKEN_TTL',
51
+ 'REFRESH_TOKEN_TTL',
52
+ 'REFRESH_TOKEN_COOKIE_DOMAIN',
53
+ 'REFRESH_TOKEN_COOKIE_SECURE',
54
+ 'REFRESH_TOKEN_COOKIE_SAME_SITE',
55
+ 'REFRESH_TOKEN_COOKIE_NAME',
56
+ 'PASSWORD_RESET_URL_ALLOW_LIST',
57
+ 'USER_INVITE_URL_ALLOW_LIST',
58
+ 'IP_TRUST_PROXY',
59
+ 'IP_CUSTOM_HEADER',
60
+ 'ASSETS_CONTENT_SECURITY_POLICY',
61
+ 'IMPORT_IP_DENY_LIST',
62
+ 'CONTENT_SECURITY_POLICY_.+',
63
+ 'HSTS_.+',
64
+ // hashing
65
+ 'HASH_MEMORY_COST',
66
+ 'HASH_LENGTH',
67
+ 'HASH_TIME_COST',
68
+ 'HASH_PARALLELISM',
69
+ 'HASH_TYPE',
70
+ 'HASH_ASSOCIATED_DATA',
71
+ // cors
72
+ 'CORS_ENABLED',
73
+ 'CORS_ORIGIN',
74
+ 'CORS_METHODS',
75
+ 'CORS_ALLOWED_HEADERS',
76
+ 'CORS_EXPOSED_HEADERS',
77
+ 'CORS_CREDENTIALS',
78
+ 'CORS_MAX_AGE',
79
+ // rate limiting
80
+ 'RATE_LIMITER_ENABLED',
81
+ 'RATE_LIMITER_POINTS',
82
+ 'RATE_LIMITER_DURATION',
83
+ 'RATE_LIMITER_STORE',
84
+ 'RATE_LIMITER_REDIS',
85
+ 'RATE_LIMITER_REDIS_HOST',
86
+ 'RATE_LIMITER_REDIS_PORT',
87
+ 'RATE_LIMITER_REDIS_PASSWORD',
88
+ 'RATE_LIMITER_MEMCACHE',
89
+ // cache
90
+ 'CACHE_ENABLED',
91
+ 'CACHE_TTL',
92
+ 'CACHE_CONTROL_S_MAXAGE',
93
+ 'CACHE_AUTO_PURGE',
94
+ 'CACHE_SYSTEM_TTL',
95
+ 'CACHE_SCHEMA',
96
+ 'CACHE_PERMISSIONS',
97
+ 'CACHE_NAMESPACE',
98
+ 'CACHE_STORE',
99
+ 'CACHE_STATUS_HEADER',
100
+ 'CACHE_REDIS',
101
+ 'CACHE_REDIS_HOST',
102
+ 'CACHE_REDIS_PORT',
103
+ 'CACHE_REDIS_PASSWORD',
104
+ 'CACHE_MEMCACHE',
105
+ // storage
106
+ 'STORAGE_LOCATIONS',
107
+ 'STORAGE_.+_DRIVER',
108
+ 'STORAGE_.+_ROOT',
109
+ 'STORAGE_.+_KEY',
110
+ 'STORAGE_.+_SECRET',
111
+ 'STORAGE_.+_BUCKET',
112
+ 'STORAGE_.+_REGION',
113
+ 'STORAGE_.+_ENDPOINT',
114
+ 'STORAGE_.+_ACL',
115
+ 'STORAGE_.+_CONTAINER_NAME',
116
+ 'STORAGE_.+_ACCOUNT_NAME',
117
+ 'STORAGE_.+_ACCOUNT_KEY',
118
+ 'STORAGE_.+_ENDPOINT',
119
+ 'STORAGE_.+_KEY_FILENAME',
120
+ 'STORAGE_.+_BUCKET',
121
+ // metadata
122
+ 'FILE_METADATA_ALLOW_LIST',
123
+ // assets
124
+ 'ASSETS_CACHE_TTL',
125
+ 'ASSETS_TRANSFORM_MAX_CONCURRENT',
126
+ 'ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION',
127
+ 'ASSETS_TRANSFORM_MAX_OPERATIONS',
128
+ 'ASSETS_CONTENT_SECURITY_POLICY',
129
+ // auth
130
+ 'AUTH_PROVIDERS',
131
+ 'AUTH_DISABLE_DEFAULT',
132
+ 'AUTH_.+_DRIVER',
133
+ 'AUTH_.+_CLIENT_ID',
134
+ 'AUTH_.+_CLIENT_SECRET',
135
+ 'AUTH_.+_SCOPE',
136
+ 'AUTH_.+_AUTHORIZE_URL',
137
+ 'AUTH_.+_ACCESS_URL',
138
+ 'AUTH_.+_PROFILE_URL',
139
+ 'AUTH_.+_IDENTIFIER_KEY',
140
+ 'AUTH_.+_EMAIL_KEY',
141
+ 'AUTH_.+_FIRST_NAME_KEY',
142
+ 'AUTH_.+_LAST_NAME_KEY',
143
+ 'AUTH_.+_ALLOW_PUBLIC_REGISTRATION',
144
+ 'AUTH_.+_DEFAULT_ROLE_ID',
145
+ 'AUTH_.+_ICON',
146
+ 'AUTH_.+_PARAMS',
147
+ 'AUTH_.+_ISSUER_URL',
148
+ 'AUTH_.+_AUTH_REQUIRE_VERIFIED_EMAIL',
149
+ 'AUTH_.+_CLIENT_URL',
150
+ 'AUTH_.+_BIND_DN',
151
+ 'AUTH_.+_BIND_PASSWORD',
152
+ 'AUTH_.+_USER_DN',
153
+ 'AUTH_.+_USER_ATTRIBUTE',
154
+ 'AUTH_.+_USER_SCOPE',
155
+ 'AUTH_.+_MAIL_ATTRIBUTE',
156
+ 'AUTH_.+_FIRST_NAME_ATTRIBUTE',
157
+ 'AUTH_.+_LAST_NAME_ATTRIBUTE',
158
+ 'AUTH_.+_GROUP_DN',
159
+ 'AUTH_.+_GROUP_ATTRIBUTE',
160
+ 'AUTH_.+_GROUP_SCOPE',
161
+ // extensions
162
+ 'EXTENSIONS_PATH',
163
+ 'EXTENSIONS_AUTO_RELOAD',
164
+ // emails
165
+ 'EMAIL_FROM',
166
+ 'EMAIL_TRANSPORT',
167
+ 'EMAIL_SENDMAIL_NEW_LINE',
168
+ 'EMAIL_SENDMAIL_PATH',
169
+ 'EMAIL_SMTP_HOST',
170
+ 'EMAIL_SMTP_PORT',
171
+ 'EMAIL_SMTP_USER',
172
+ 'EMAIL_SMTP_PASSWORD',
173
+ 'EMAIL_SMTP_POOL',
174
+ 'EMAIL_SMTP_SECURE',
175
+ 'EMAIL_SMTP_IGNORE_TLS',
176
+ 'EMAIL_MAILGUN_API_KEY',
177
+ 'EMAIL_MAILGUN_DOMAIN',
178
+ 'EMAIL_MAILGUN_HOST',
179
+ 'EMAIL_SES_CREDENTIALS__ACCESS_KEY_ID',
180
+ 'EMAIL_SES_CREDENTIALS__SECRET_ACCESS_KEY',
181
+ 'EMAIL_SES_REGION',
182
+ // admin account
183
+ 'ADMIN_EMAIL',
184
+ 'ADMIN_PASSWORD',
185
+ // telemetry
186
+ 'TELEMETRY',
187
+ // limits & optimization
188
+ 'RELATIONAL_BATCH_SIZE',
189
+ 'EXPORT_BATCH_SIZE',
190
+ ].map((name) => new RegExp(`^${name}$`));
18
191
  const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
19
192
  const defaults = {
20
193
  CONFIG_PATH: path_1.default.resolve(process.cwd(), '.env'),
@@ -180,15 +353,17 @@ function processValues(env) {
180
353
  let newKey;
181
354
  if (key.length > 5 && key.endsWith('_FILE')) {
182
355
  newKey = key.slice(0, -5);
183
- if (newKey in env) {
184
- throw new Error(`Duplicate environment variable encountered: you can't use "${newKey}" and "${key}" simultaneously.`);
185
- }
186
- try {
187
- value = fs_1.default.readFileSync(value, { encoding: 'utf8' });
188
- key = newKey;
189
- }
190
- catch {
191
- throw new Error(`Failed to read value from file "${value}", defined in environment variable "${key}".`);
356
+ if (allowedEnvironmentVars.some((pattern) => pattern.test(newKey))) {
357
+ if (newKey in env) {
358
+ throw new Error(`Duplicate environment variable encountered: you can't use "${newKey}" and "${key}" simultaneously.`);
359
+ }
360
+ try {
361
+ value = fs_1.default.readFileSync(value, { encoding: 'utf8' });
362
+ key = newKey;
363
+ }
364
+ catch {
365
+ throw new Error(`Failed to read value from file "${value}", defined in environment variable "${key}".`);
366
+ }
192
367
  }
193
368
  }
194
369
  // Convert values with a type prefix
@@ -213,7 +388,7 @@ function processValues(env) {
213
388
  env[key] = tryJSON(value);
214
389
  break;
215
390
  case 'boolean':
216
- env[key] = value === 'true' || value === true || value === '1' || value === 1;
391
+ env[key] = toBoolean(value);
217
392
  }
218
393
  continue;
219
394
  }
@@ -262,3 +437,6 @@ function tryJSON(value) {
262
437
  return value;
263
438
  }
264
439
  }
440
+ function toBoolean(value) {
441
+ return value === 'true' || value === true || value === '1' || value === 1;
442
+ }
@@ -17,6 +17,7 @@ const get_ast_from_query_1 = __importDefault(require("../utils/get-ast-from-quer
17
17
  const authorization_1 = require("./authorization");
18
18
  const index_1 = require("./index");
19
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;
@@ -244,6 +245,7 @@ class ItemsService {
244
245
  */
245
246
  async readOne(key, query = {}, opts) {
246
247
  const primaryKeyField = this.schema.collections[this.collection].primary;
248
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
247
249
  const filterWithKey = (0, lodash_1.assign)({}, query.filter, { [primaryKeyField]: { _eq: key } });
248
250
  const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
249
251
  const results = await this.readByQuery(queryWithKey, opts);
@@ -258,6 +260,7 @@ class ItemsService {
258
260
  async readMany(keys, query = {}, opts) {
259
261
  var _a;
260
262
  const primaryKeyField = this.schema.collections[this.collection].primary;
263
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
261
264
  const filterWithKey = { _and: [{ [primaryKeyField]: { _in: keys } }, (_a = query.filter) !== null && _a !== void 0 ? _a : {}] };
262
265
  const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
263
266
  // Set query limit as the number of keys
@@ -272,12 +275,16 @@ class ItemsService {
272
275
  */
273
276
  async updateByQuery(query, data, opts) {
274
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);
275
280
  return keys.length ? await this.updateMany(keys, data, opts) : [];
276
281
  }
277
282
  /**
278
283
  * Update a single item by primary key
279
284
  */
280
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);
281
288
  await this.updateMany([key], data, opts);
282
289
  return key;
283
290
  }
@@ -286,6 +293,7 @@ class ItemsService {
286
293
  */
287
294
  async updateMany(keys, data, opts) {
288
295
  const primaryKeyField = this.schema.collections[this.collection].primary;
296
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
289
297
  const fields = Object.keys(this.schema.collections[this.collection].fields);
290
298
  const aliases = Object.values(this.schema.collections[this.collection].fields)
291
299
  .filter((field) => field.alias === true)
@@ -415,6 +423,9 @@ class ItemsService {
415
423
  async upsertOne(payload, opts) {
416
424
  const primaryKeyField = this.schema.collections[this.collection].primary;
417
425
  const primaryKey = payload[primaryKeyField];
426
+ if (primaryKey) {
427
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, primaryKey);
428
+ }
418
429
  const exists = primaryKey &&
419
430
  !!(await this.knex
420
431
  .select(primaryKeyField)
@@ -455,12 +466,16 @@ class ItemsService {
455
466
  */
456
467
  async deleteByQuery(query, opts) {
457
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);
458
471
  return keys.length ? await this.deleteMany(keys, opts) : [];
459
472
  }
460
473
  /**
461
474
  * Delete a single item by primary key
462
475
  */
463
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);
464
479
  await this.deleteMany([key], opts);
465
480
  return key;
466
481
  }
@@ -469,6 +484,7 @@ class ItemsService {
469
484
  */
470
485
  async deleteMany(keys, opts) {
471
486
  const primaryKeyField = this.schema.collections[this.collection].primary;
487
+ (0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
472
488
  if (this.accountability && this.accountability.admin !== true) {
473
489
  const authorizationService = new authorization_1.AuthorizationService({
474
490
  accountability: this.accountability,
@@ -7,12 +7,12 @@ exports.applyAggregate = exports.applySearch = exports.applyFilter = exports.app
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 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");
15
- const operators_1 = __importDefault(require("./operators"));
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");
15
+ const utils_1 = require("@directus/shared/utils");
16
16
  const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz', 5);
17
17
  /**
18
18
  * Apply the Query to a given Knex query builder instance
@@ -155,9 +155,6 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
155
155
  addJoins(rootQuery, rootFilter, collection);
156
156
  addWhereClauses(knex, rootQuery, rootFilter, collection);
157
157
  return rootQuery;
158
- function isNegativeOperator(operator) {
159
- return operator.indexOf('_n') === 0;
160
- }
161
158
  function addJoins(dbQuery, filter, collection) {
162
159
  for (const [key, value] of Object.entries(filter)) {
163
160
  if (key === '_or' || key === '_and') {
@@ -185,33 +182,8 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
185
182
  }
186
183
  }
187
184
  }
188
- function callbackSubqueryRelation(relation, value) {
189
- return function (subQueryKnex) {
190
- const field = relation.field;
191
- const collection = relation.collection;
192
- const column = `${collection}.${field}`;
193
- subQueryKnex.from(collection).whereRaw(`${field} = ${column}`);
194
- applyQuery(knex, relation.collection, subQueryKnex, {
195
- filter: value,
196
- }, schema, true);
197
- };
198
- }
199
- function inverseFilters(value) {
200
- for (const field in value) {
201
- for (const operator in value[field]) {
202
- let inverseOperator = operator;
203
- if (isNegativeOperator(operator)) {
204
- inverseOperator = '_' + operator.substring(2);
205
- }
206
- else {
207
- inverseOperator = '_n' + operator.substring(1);
208
- }
209
- value[field][inverseOperator] = value[field][operator];
210
- delete value[field][operator];
211
- }
212
- }
213
- }
214
185
  function addWhereClauses(knex, dbQuery, filter, collection, logical = 'and') {
186
+ var _a, _b;
215
187
  for (const [key, value] of Object.entries(filter)) {
216
188
  if (key === '_or' || key === '_and') {
217
189
  // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
@@ -252,12 +224,24 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
252
224
  if (relationType === 'o2a') {
253
225
  pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
254
226
  }
255
- if (isNegativeOperator(filterOperator)) {
256
- inverseFilters(value);
257
- dbQuery[logical].whereNotExists(callbackSubqueryRelation(relation, value));
227
+ const subQueryBuilder = (filter) => (subQueryKnex) => {
228
+ const field = relation.field;
229
+ const collection = relation.collection;
230
+ const column = `${collection}.${field}`;
231
+ subQueryKnex
232
+ .select({ [field]: column })
233
+ .from(collection)
234
+ .whereNotNull(column);
235
+ applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, true);
236
+ };
237
+ if (((_a = Object.keys(value)) === null || _a === void 0 ? void 0 : _a[0]) === '_none') {
238
+ dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0]));
239
+ }
240
+ else if (((_b = Object.keys(value)) === null || _b === void 0 ? void 0 : _b[0]) === '_some') {
241
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0]));
258
242
  }
259
243
  else {
260
- dbQuery[logical].whereExists(callbackSubqueryRelation(relation, value));
244
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(value));
261
245
  }
262
246
  }
263
247
  }
@@ -267,6 +251,32 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
267
251
  const selectionRaw = (0, get_column_1.getColumn)(knex, table, column, false, schema);
268
252
  // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad..
269
253
  // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated
254
+ // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`)
255
+ if (operator === '_null' || (operator === '_nnull' && compareValue === false)) {
256
+ dbQuery[logical].whereNull(selectionRaw);
257
+ }
258
+ if (operator === '_nnull' || (operator === '_null' && compareValue === false)) {
259
+ dbQuery[logical].whereNotNull(selectionRaw);
260
+ }
261
+ if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) {
262
+ dbQuery[logical].andWhere((query) => {
263
+ query.where(key, '=', '');
264
+ });
265
+ }
266
+ if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) {
267
+ dbQuery[logical].andWhere((query) => {
268
+ query.where(key, '!=', '');
269
+ });
270
+ }
271
+ // Cast filter value (compareValue) based on function used
272
+ if (column.includes('(') && column.includes(')')) {
273
+ const functionName = column.split('(')[0];
274
+ const type = (0, utils_1.getOutputTypeForFunction)(functionName);
275
+ if (['bigInteger', 'integer', 'float', 'decimal'].includes(type)) {
276
+ compareValue = Number(compareValue);
277
+ }
278
+ }
279
+ // Cast filter value (compareValue) based on type of field being filtered against
270
280
  const [collection, field] = key.split('.');
271
281
  if (collection in schema.collections && field in schema.collections[collection].fields) {
272
282
  const type = schema.collections[collection].fields[field].type;
@@ -298,16 +308,105 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
298
308
  // We need to remove any undefined values, as they are useless
299
309
  compareValue = compareValue.filter((val) => val !== undefined);
300
310
  }
301
- if (operator in operators_1.default) {
302
- operators_1.default[operator].apply({
303
- query: dbQuery[logical],
304
- helpers,
305
- selectionRaw,
306
- compareValue,
307
- });
311
+ if (operator === '_eq') {
312
+ dbQuery[logical].where(selectionRaw, '=', compareValue);
308
313
  }
309
- else {
310
- throw new Error(`Operator ${operator} not supported`);
314
+ if (operator === '_neq') {
315
+ dbQuery[logical].whereNot(selectionRaw, compareValue);
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
+ }
323
+ if (operator === '_contains') {
324
+ dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
325
+ }
326
+ if (operator === '_ncontains') {
327
+ dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
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
+ }
335
+ if (operator === '_starts_with') {
336
+ dbQuery[logical].where(key, 'like', `${compareValue}%`);
337
+ }
338
+ if (operator === '_nstarts_with') {
339
+ dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
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
+ }
347
+ if (operator === '_ends_with') {
348
+ dbQuery[logical].where(key, 'like', `%${compareValue}`);
349
+ }
350
+ if (operator === '_nends_with') {
351
+ dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
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
+ }
359
+ if (operator === '_gt') {
360
+ dbQuery[logical].where(selectionRaw, '>', compareValue);
361
+ }
362
+ if (operator === '_gte') {
363
+ dbQuery[logical].where(selectionRaw, '>=', compareValue);
364
+ }
365
+ if (operator === '_lt') {
366
+ dbQuery[logical].where(selectionRaw, '<', compareValue);
367
+ }
368
+ if (operator === '_lte') {
369
+ dbQuery[logical].where(selectionRaw, '<=', compareValue);
370
+ }
371
+ if (operator === '_in') {
372
+ let value = compareValue;
373
+ if (typeof value === 'string')
374
+ value = value.split(',');
375
+ dbQuery[logical].whereIn(selectionRaw, value);
376
+ }
377
+ if (operator === '_nin') {
378
+ let value = compareValue;
379
+ if (typeof value === 'string')
380
+ value = value.split(',');
381
+ dbQuery[logical].whereNotIn(selectionRaw, value);
382
+ }
383
+ if (operator === '_between') {
384
+ if (compareValue.length !== 2)
385
+ return;
386
+ let value = compareValue;
387
+ if (typeof value === 'string')
388
+ value = value.split(',');
389
+ dbQuery[logical].whereBetween(selectionRaw, value);
390
+ }
391
+ if (operator === '_nbetween') {
392
+ if (compareValue.length !== 2)
393
+ return;
394
+ let value = compareValue;
395
+ if (typeof value === 'string')
396
+ value = value.split(',');
397
+ dbQuery[logical].whereNotBetween(selectionRaw, value);
398
+ }
399
+ if (operator == '_intersects') {
400
+ dbQuery[logical].whereRaw(helpers.st.intersects(key, compareValue));
401
+ }
402
+ if (operator == '_nintersects') {
403
+ dbQuery[logical].whereRaw(helpers.st.nintersects(key, compareValue));
404
+ }
405
+ if (operator == '_intersects_bbox') {
406
+ dbQuery[logical].whereRaw(helpers.st.intersects_bbox(key, compareValue));
407
+ }
408
+ if (operator == '_nintersects_bbox') {
409
+ dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
311
410
  }
312
411
  }
313
412
  }
@@ -0,0 +1,6 @@
1
+ import { SchemaOverview } from '@directus/shared/types';
2
+ import { PrimaryKey } from '../types';
3
+ /**
4
+ * Validate keys based on its type
5
+ */
6
+ export declare function validateKeys(schema: SchemaOverview, collection: string, keyField: string, keys: PrimaryKey | PrimaryKey[]): void;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateKeys = void 0;
7
+ const exceptions_1 = require("../exceptions");
8
+ const uuid_validate_1 = __importDefault(require("uuid-validate"));
9
+ /**
10
+ * Validate keys based on its type
11
+ */
12
+ function validateKeys(schema, collection, keyField, keys) {
13
+ if (Array.isArray(keys)) {
14
+ for (const key of keys) {
15
+ validateKeys(schema, collection, keyField, key);
16
+ }
17
+ }
18
+ else {
19
+ const primaryKeyFieldType = schema.collections[collection].fields[keyField].type;
20
+ if (primaryKeyFieldType === 'uuid' && !(0, uuid_validate_1.default)(String(keys))) {
21
+ throw new exceptions_1.ForbiddenException();
22
+ }
23
+ else if (primaryKeyFieldType === 'integer' && !Number.isInteger(Number(keys))) {
24
+ throw new exceptions_1.ForbiddenException();
25
+ }
26
+ }
27
+ }
28
+ exports.validateKeys = validateKeys;
@@ -104,7 +104,7 @@ function validateFilterPrimitive(value, key) {
104
104
  false) {
105
105
  throw new exceptions_1.InvalidQueryException(`The filter value for "${key}" has to be a string, number, or boolean`);
106
106
  }
107
- if (typeof value === 'number' && (Number.isNaN(value) || !Number.isSafeInteger(value))) {
107
+ if (typeof value === 'number' && Number.isNaN(value) && value <= Number.MAX_SAFE_INTEGER) {
108
108
  throw new exceptions_1.InvalidQueryException(`The filter value for "${key}" is not a valid number`);
109
109
  }
110
110
  if (typeof value === 'string' && value.length === 0) {