directus 9.13.0 → 9.14.2

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 (61) hide show
  1. package/dist/app.js +9 -4
  2. package/dist/auth/drivers/openid.js +4 -1
  3. package/dist/cli/commands/security/key.d.ts +1 -0
  4. package/dist/cli/commands/security/key.js +8 -0
  5. package/dist/cli/commands/security/secret.d.ts +1 -0
  6. package/dist/cli/commands/security/secret.js +8 -0
  7. package/dist/cli/index.js +6 -0
  8. package/dist/controllers/extensions.js +9 -1
  9. package/dist/controllers/files.js +1 -1
  10. package/dist/database/helpers/fn/dialects/mssql.d.ts +2 -2
  11. package/dist/database/helpers/fn/dialects/mssql.js +2 -2
  12. package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -2
  13. package/dist/database/helpers/fn/dialects/mysql.js +2 -2
  14. package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -1
  15. package/dist/database/helpers/fn/dialects/oracle.js +2 -2
  16. package/dist/database/helpers/fn/dialects/postgres.d.ts +2 -2
  17. package/dist/database/helpers/fn/dialects/postgres.js +2 -2
  18. package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -1
  19. package/dist/database/helpers/fn/dialects/sqlite.js +2 -2
  20. package/dist/database/helpers/fn/types.d.ts +3 -2
  21. package/dist/database/helpers/fn/types.js +11 -8
  22. package/dist/database/helpers/index.d.ts +3 -3
  23. package/dist/database/helpers/schema/dialects/sqlite.d.ts +5 -0
  24. package/dist/database/helpers/schema/dialects/sqlite.js +17 -0
  25. package/dist/database/helpers/schema/index.d.ts +1 -1
  26. package/dist/database/helpers/schema/index.js +4 -4
  27. package/dist/database/helpers/schema/types.d.ts +2 -0
  28. package/dist/database/helpers/schema/types.js +6 -0
  29. package/dist/database/run-ast.js +8 -5
  30. package/dist/emitter.js +12 -7
  31. package/dist/logger.d.ts +0 -1
  32. package/dist/middleware/authenticate.d.ts +0 -1
  33. package/dist/middleware/cache.js +3 -3
  34. package/dist/middleware/graphql.js +9 -7
  35. package/dist/middleware/respond.js +5 -5
  36. package/dist/operations/item-read/index.d.ts +1 -0
  37. package/dist/operations/item-read/index.js +3 -3
  38. package/dist/operations/request/index.js +1 -1
  39. package/dist/services/authorization.js +15 -5
  40. package/dist/services/fields.js +4 -0
  41. package/dist/services/graphql/index.js +40 -15
  42. package/dist/services/import-export.js +6 -1
  43. package/dist/services/items.d.ts +1 -0
  44. package/dist/services/items.js +21 -17
  45. package/dist/services/notifications.js +22 -11
  46. package/dist/services/payload.js +11 -8
  47. package/dist/services/server.js +13 -2
  48. package/dist/types/ast.d.ts +11 -4
  49. package/dist/utils/apply-query.js +5 -13
  50. package/dist/utils/apply-snapshot.js +41 -17
  51. package/dist/utils/filter-items.js +3 -6
  52. package/dist/utils/get-ast-from-query.js +18 -0
  53. package/dist/utils/get-column-path.d.ts +5 -1
  54. package/dist/utils/get-column-path.js +3 -1
  55. package/dist/utils/get-column.d.ts +2 -2
  56. package/dist/utils/get-column.js +2 -2
  57. package/dist/utils/get-config-from-env.js +1 -1
  58. package/dist/utils/merge-permissions.d.ts +0 -1
  59. package/package.json +232 -226
  60. package/dist/utils/generate-joi.d.ts +0 -3
  61. package/dist/utils/generate-joi.js +0 -145
@@ -6,7 +6,7 @@ const get_accountability_for_role_1 = require("../../utils/get-accountability-fo
6
6
  const sanitize_query_1 = require("../../utils/sanitize-query");
7
7
  exports.default = (0, utils_1.defineOperationApi)({
8
8
  id: 'item-read',
9
- handler: async ({ collection, key, query, permissions }, { accountability, database, getSchema }) => {
9
+ handler: async ({ collection, key, query, emitEvents, permissions }, { accountability, database, getSchema }) => {
10
10
  const schema = await getSchema({ database });
11
11
  let customAccountability;
12
12
  if (!permissions || permissions === '$trigger') {
@@ -35,10 +35,10 @@ exports.default = (0, utils_1.defineOperationApi)({
35
35
  else {
36
36
  const keys = (0, utils_1.toArray)(key);
37
37
  if (keys.length === 1) {
38
- result = await itemsService.readOne(keys[0], sanitizedQueryObject);
38
+ result = await itemsService.readOne(keys[0], sanitizedQueryObject, { emitEvents });
39
39
  }
40
40
  else {
41
- result = await itemsService.readMany(keys, sanitizedQueryObject);
41
+ result = await itemsService.readMany(keys, sanitizedQueryObject, { emitEvents });
42
42
  }
43
43
  }
44
44
  return result;
@@ -12,7 +12,7 @@ exports.default = (0, utils_1.defineOperationApi)({
12
12
  acc[header] = value;
13
13
  return acc;
14
14
  }, {});
15
- const result = await (0, axios_1.default)({ url, method, data: body, headers: customHeaders });
15
+ const result = await (0, axios_1.default)({ url: encodeURI(url), method, data: body, headers: customHeaders });
16
16
  return { status: result.status, statusText: result.statusText, headers: result.headers, data: result.data };
17
17
  },
18
18
  });
@@ -48,7 +48,7 @@ class AuthorizationService {
48
48
  collections.push(...ast.names.map((name) => ({ collection: name, field: ast.fieldKey })));
49
49
  for (const children of Object.values(ast.children)) {
50
50
  for (const nestedNode of children) {
51
- if (nestedNode.type !== 'field') {
51
+ if (nestedNode.type !== 'field' && nestedNode.type !== 'functionField') {
52
52
  collections.push(...getCollectionsFromAST(nestedNode));
53
53
  }
54
54
  }
@@ -60,7 +60,13 @@ class AuthorizationService {
60
60
  field: ast.type === 'root' ? null : ast.fieldKey,
61
61
  });
62
62
  for (const nestedNode of ast.children) {
63
- if (nestedNode.type !== 'field') {
63
+ if (nestedNode.type === 'functionField') {
64
+ collections.push({
65
+ collection: nestedNode.relatedCollection,
66
+ field: null,
67
+ });
68
+ }
69
+ else if (nestedNode.type !== 'field') {
64
70
  collections.push(...getCollectionsFromAST(nestedNode));
65
71
  }
66
72
  }
@@ -69,7 +75,7 @@ class AuthorizationService {
69
75
  }
70
76
  function validateFields(ast) {
71
77
  var _a, _b, _c;
72
- if (ast.type !== 'field') {
78
+ if (ast.type !== 'field' && ast.type !== 'functionField') {
73
79
  if (ast.type === 'a2o') {
74
80
  for (const [collection, children] of Object.entries(ast.children)) {
75
81
  checkFields(collection, children, (_b = (_a = ast.query) === null || _a === void 0 ? void 0 : _a[collection]) === null || _b === void 0 ? void 0 : _b.aggregate);
@@ -112,7 +118,7 @@ class AuthorizationService {
112
118
  function validateFilterPermissions(ast, schema, action, accountability) {
113
119
  var _a, _b, _c, _d, _e;
114
120
  let requiredFieldPermissions = {};
115
- if (ast.type !== 'field') {
121
+ if (ast.type !== 'field' && ast.type !== 'functionField') {
116
122
  if (ast.type === 'a2o') {
117
123
  for (const collection of Object.keys(ast.children)) {
118
124
  requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, extractRequiredFieldPermissions(collection, (_c = (_b = (_a = ast.query) === null || _a === void 0 ? void 0 : _a[collection]) === null || _b === void 0 ? void 0 : _b.filter) !== null && _c !== void 0 ? _c : {}));
@@ -290,7 +296,11 @@ class AuthorizationService {
290
296
  }
291
297
  }
292
298
  function applyFilters(ast, accountability) {
293
- if (ast.type !== 'field') {
299
+ if (ast.type === 'functionField') {
300
+ const collection = ast.relatedCollection;
301
+ updateFilterQuery(collection, ast.query);
302
+ }
303
+ else if (ast.type !== 'field') {
294
304
  if (ast.type === 'a2o') {
295
305
  const collections = Object.keys(ast.children);
296
306
  for (const collection of collections) {
@@ -351,6 +351,7 @@ class FieldsService {
351
351
  if (this.accountability && this.accountability.admin !== true) {
352
352
  throw new exceptions_1.ForbiddenException();
353
353
  }
354
+ const runPostColumnDelete = await this.helpers.schema.preColumnDelete();
354
355
  try {
355
356
  await emitter_1.default.emitFilter('fields.delete', [field], {
356
357
  collection: collection,
@@ -439,6 +440,9 @@ class FieldsService {
439
440
  });
440
441
  }
441
442
  finally {
443
+ if (runPostColumnDelete) {
444
+ await this.helpers.schema.postColumnDelete();
445
+ }
442
446
  if (this.cache && env_1.default.CACHE_AUTO_PURGE) {
443
447
  await this.cache.clear();
444
448
  }
@@ -12,6 +12,7 @@ const lodash_1 = require("lodash");
12
12
  const ms_1 = __importDefault(require("ms"));
13
13
  const cache_1 = require("../../cache");
14
14
  const constants_1 = require("../../constants");
15
+ const constants_2 = require("@directus/shared/constants");
15
16
  const database_1 = __importDefault(require("../../database"));
16
17
  const env_1 = __importDefault(require("../../env"));
17
18
  const exceptions_1 = require("../../exceptions");
@@ -488,6 +489,12 @@ class GraphQLService {
488
489
  _nnull: {
489
490
  type: graphql_1.GraphQLBoolean,
490
491
  },
492
+ _between: {
493
+ type: new graphql_1.GraphQLList(string_or_float_1.GraphQLStringOrFloat),
494
+ },
495
+ _nbetween: {
496
+ type: new graphql_1.GraphQLList(string_or_float_1.GraphQLStringOrFloat),
497
+ },
491
498
  },
492
499
  });
493
500
  // Uses StringOrFloat rather than Float to support api dynamic variables (like `$NOW`)
@@ -524,6 +531,12 @@ class GraphQLService {
524
531
  _nnull: {
525
532
  type: graphql_1.GraphQLBoolean,
526
533
  },
534
+ _between: {
535
+ type: new graphql_1.GraphQLList(string_or_float_1.GraphQLStringOrFloat),
536
+ },
537
+ _nbetween: {
538
+ type: new graphql_1.GraphQLList(string_or_float_1.GraphQLStringOrFloat),
539
+ },
527
540
  },
528
541
  });
529
542
  const GeometryFilterOperators = schemaComposer.createInputTC({
@@ -671,6 +684,16 @@ class GraphQLService {
671
684
  return acc;
672
685
  }, {}),
673
686
  });
687
+ const countType = schemaComposer.createObjectTC({
688
+ name: `${collection.collection}_aggregated_count`,
689
+ fields: Object.values(collection.fields).reduce((acc, field) => {
690
+ acc[field.field] = {
691
+ type: graphql_1.GraphQLInt,
692
+ description: field.note,
693
+ };
694
+ return acc;
695
+ }, {}),
696
+ });
674
697
  AggregateMethods[collection.collection] = {
675
698
  group: {
676
699
  name: 'group',
@@ -682,16 +705,11 @@ class GraphQLService {
682
705
  },
683
706
  count: {
684
707
  name: 'count',
685
- type: schemaComposer.createObjectTC({
686
- name: `${collection.collection}_aggregated_count`,
687
- fields: Object.values(collection.fields).reduce((acc, field) => {
688
- acc[field.field] = {
689
- type: graphql_1.GraphQLInt,
690
- description: field.note,
691
- };
692
- return acc;
693
- }, {}),
694
- }),
708
+ type: countType,
709
+ },
710
+ countDistinct: {
711
+ name: 'countDistinct',
712
+ type: countType,
695
713
  },
696
714
  };
697
715
  const hasNumericAggregates = Object.values(collection.fields).some((field) => {
@@ -711,10 +729,6 @@ class GraphQLService {
711
729
  name: 'sum',
712
730
  type: AggregatedFields[collection.collection],
713
731
  },
714
- countDistinct: {
715
- name: 'countDistinct',
716
- type: AggregatedFields[collection.collection],
717
- },
718
732
  avgDistinct: {
719
733
  name: 'avgDistinct',
720
734
  type: AggregatedFields[collection.collection],
@@ -1042,6 +1056,17 @@ class GraphQLService {
1042
1056
  };
1043
1057
  query.limit = 1;
1044
1058
  }
1059
+ // Transform count(a.b.c) into a.b.count(c)
1060
+ for (let fieldIndex = 0; fieldIndex < query.fields.length; fieldIndex++) {
1061
+ if (query.fields[fieldIndex].includes('(') && query.fields[fieldIndex].includes(')')) {
1062
+ const functionName = query.fields[fieldIndex].split('(')[0];
1063
+ const columnNames = query.fields[fieldIndex].match(constants_2.REGEX_BETWEEN_PARENS)[1].split('.');
1064
+ if (columnNames.length > 1) {
1065
+ const column = columnNames.pop();
1066
+ query.fields[fieldIndex] = columnNames.join('.') + '.' + functionName + '(' + column + ')';
1067
+ }
1068
+ }
1069
+ }
1045
1070
  const result = await this.read(collection, query);
1046
1071
  if (args.id) {
1047
1072
  return (result === null || result === void 0 ? void 0 : result[0]) || null;
@@ -1288,7 +1313,7 @@ class GraphQLService {
1288
1313
  result[currentKey] = Object.values(value)[0];
1289
1314
  }
1290
1315
  else {
1291
- result[currentKey] = (0, lodash_1.isObject)(value) ? replaceFuncDeep(value) : value;
1316
+ result[currentKey] = (value === null || value === void 0 ? void 0 : value.constructor) === Object ? replaceFuncDeep(value) : value;
1292
1317
  }
1293
1318
  });
1294
1319
  }
@@ -102,7 +102,12 @@ class ImportService {
102
102
  else {
103
103
  try {
104
104
  const parsedJson = (0, utils_1.parseJSON)(value);
105
- (0, lodash_1.set)(result, key, parsedJson);
105
+ if (typeof parsedJson === 'number') {
106
+ (0, lodash_1.set)(result, key, value);
107
+ }
108
+ else {
109
+ (0, lodash_1.set)(result, key, parsedJson);
110
+ }
106
111
  }
107
112
  catch {
108
113
  (0, lodash_1.set)(result, key, value);
@@ -5,6 +5,7 @@ import { AbstractService, AbstractServiceOptions, Item as AnyItem, MutationOptio
5
5
  export declare type QueryOptions = {
6
6
  stripNonRequested?: boolean;
7
7
  permissionsAction?: PermissionsAction;
8
+ emitEvents?: boolean;
8
9
  };
9
10
  export declare class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
10
11
  collection: string;
@@ -221,23 +221,27 @@ class ItemsService {
221
221
  if (records === null) {
222
222
  throw new exceptions_1.ForbiddenException();
223
223
  }
224
- const filteredRecords = await emitter_1.default.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
225
- query,
226
- collection: this.collection,
227
- }, {
228
- database: this.knex,
229
- schema: this.schema,
230
- accountability: this.accountability,
231
- });
232
- emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, {
233
- payload: filteredRecords,
234
- query,
235
- collection: this.collection,
236
- }, {
237
- database: this.knex || (0, database_1.default)(),
238
- schema: this.schema,
239
- accountability: this.accountability,
240
- });
224
+ const filteredRecords = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
225
+ ? await emitter_1.default.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
226
+ query,
227
+ collection: this.collection,
228
+ }, {
229
+ database: this.knex,
230
+ schema: this.schema,
231
+ accountability: this.accountability,
232
+ })
233
+ : records;
234
+ if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
235
+ emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, {
236
+ payload: filteredRecords,
237
+ query,
238
+ collection: this.collection,
239
+ }, {
240
+ database: this.knex || (0, database_1.default)(),
241
+ schema: this.schema,
242
+ accountability: this.accountability,
243
+ });
244
+ }
241
245
  return filteredRecords;
242
246
  }
243
247
  /**
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.NotificationsService = void 0;
4
7
  const items_1 = require("./items");
5
8
  const md_1 = require("../utils/md");
6
9
  const users_1 = require("./users");
7
10
  const mail_1 = require("./mail");
11
+ const logger_1 = __importDefault(require("../logger"));
8
12
  class NotificationsService extends items_1.ItemsService {
9
13
  constructor(options) {
10
14
  super('directus_notifications', options);
@@ -12,29 +16,36 @@ class NotificationsService extends items_1.ItemsService {
12
16
  this.mailService = new mail_1.MailService({ schema: this.schema, accountability: this.accountability });
13
17
  }
14
18
  async createOne(data, opts) {
19
+ const response = await super.createOne(data, opts);
15
20
  await this.sendEmail(data);
16
- return super.createOne(data, opts);
21
+ return response;
17
22
  }
18
23
  async createMany(data, opts) {
24
+ const response = await super.createMany(data, opts);
19
25
  for (const notification of data) {
20
26
  await this.sendEmail(notification);
21
27
  }
22
- return super.createMany(data, opts);
28
+ return response;
23
29
  }
24
30
  async sendEmail(data) {
25
31
  if (data.recipient) {
26
32
  const user = await this.usersService.readOne(data.recipient, { fields: ['email', 'email_notifications'] });
27
33
  if (user.email && user.email_notifications === true) {
28
- await this.mailService.send({
29
- template: {
30
- name: 'base',
31
- data: {
32
- html: data.message ? (0, md_1.md)(data.message) : '',
34
+ try {
35
+ await this.mailService.send({
36
+ template: {
37
+ name: 'base',
38
+ data: {
39
+ html: data.message ? (0, md_1.md)(data.message) : '',
40
+ },
33
41
  },
34
- },
35
- to: user.email,
36
- subject: data.subject,
37
- });
42
+ to: user.email,
43
+ subject: data.subject,
44
+ });
45
+ }
46
+ catch (error) {
47
+ logger_1.default.error(error.message);
48
+ }
38
49
  }
39
50
  }
40
51
  }
@@ -4,8 +4,8 @@ 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");
8
7
  const date_fns_1 = require("date-fns");
8
+ const utils_1 = require("@directus/shared/utils");
9
9
  const flat_1 = require("flat");
10
10
  const joi_1 = __importDefault(require("joi"));
11
11
  const lodash_1 = require("lodash");
@@ -259,15 +259,18 @@ class PayloadService {
259
259
  else {
260
260
  if (value instanceof Date === false && typeof value === 'string') {
261
261
  if (dateColumn.type === 'date') {
262
- const [date] = value.split('T');
263
- const [year, month, day] = date.split('-');
264
- payload[name] = new Date(Number(year), Number(month) - 1, Number(day));
262
+ const parsedDate = (0, date_fns_1.parseISO)(value);
263
+ if (!(0, date_fns_1.isValid)(parsedDate)) {
264
+ throw new exceptions_1.InvalidPayloadException(`Invalid Date format in field "${dateColumn.field}"`);
265
+ }
266
+ payload[name] = parsedDate;
265
267
  }
266
268
  if (dateColumn.type === 'dateTime') {
267
- const [date, time] = value.split('T');
268
- const [year, month, day] = date.split('-');
269
- const [hours, minutes, seconds] = time.substring(0, 8).split(':');
270
- payload[name] = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
269
+ const parsedDate = (0, date_fns_1.parseISO)(value);
270
+ if (!(0, date_fns_1.isValid)(parsedDate)) {
271
+ throw new exceptions_1.InvalidPayloadException(`Invalid DateTime format in field "${dateColumn.field}"`);
272
+ }
273
+ payload[name] = parsedDate;
271
274
  }
272
275
  if (dateColumn.type === 'timestamp') {
273
276
  const newValue = this.helpers.date.writeTimestamp(value);
@@ -51,7 +51,7 @@ class ServerService {
51
51
  this.settingsService = new settings_1.SettingsService({ knex: this.knex, schema: this.schema });
52
52
  }
53
53
  async serverInfo() {
54
- var _a;
54
+ var _a, _b;
55
55
  const info = {};
56
56
  const projectInfo = await this.settingsService.readSingleton({
57
57
  fields: [
@@ -67,7 +67,18 @@ class ServerService {
67
67
  ],
68
68
  });
69
69
  info.project = projectInfo;
70
- if (((_a = this.accountability) === null || _a === void 0 ? void 0 : _a.admin) === true) {
70
+ if ((_a = this.accountability) === null || _a === void 0 ? void 0 : _a.user) {
71
+ if (env_1.default.RATE_LIMITER_ENABLED) {
72
+ info.rateLimit = {
73
+ points: env_1.default.RATE_LIMITER_POINTS,
74
+ duration: env_1.default.RATE_LIMITER_DURATION,
75
+ };
76
+ }
77
+ else {
78
+ info.rateLimit = false;
79
+ }
80
+ }
81
+ if (((_b = this.accountability) === null || _b === void 0 ? void 0 : _b.admin) === true) {
71
82
  const osType = os_1.default.type() === 'Darwin' ? 'macOS' : os_1.default.type();
72
83
  const osVersion = osType === 'macOS' ? `${(0, macos_release_1.default)().name} (${(0, macos_release_1.default)().version})` : os_1.default.release();
73
84
  info.directus = {
@@ -2,7 +2,7 @@ import { Query, Relation } from '@directus/shared/types';
2
2
  export declare type M2ONode = {
3
3
  type: 'm2o';
4
4
  name: string;
5
- children: (NestedCollectionNode | FieldNode)[];
5
+ children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[];
6
6
  query: Query;
7
7
  fieldKey: string;
8
8
  relation: Relation;
@@ -13,7 +13,7 @@ export declare type A2MNode = {
13
13
  type: 'a2o';
14
14
  names: string[];
15
15
  children: {
16
- [collection: string]: (NestedCollectionNode | FieldNode)[];
16
+ [collection: string]: (NestedCollectionNode | FieldNode | FunctionFieldNode)[];
17
17
  };
18
18
  query: {
19
19
  [collection: string]: Query;
@@ -28,7 +28,7 @@ export declare type A2MNode = {
28
28
  export declare type O2MNode = {
29
29
  type: 'o2m';
30
30
  name: string;
31
- children: (NestedCollectionNode | FieldNode)[];
31
+ children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[];
32
32
  query: Query;
33
33
  fieldKey: string;
34
34
  relation: Relation;
@@ -41,9 +41,16 @@ export declare type FieldNode = {
41
41
  name: string;
42
42
  fieldKey: string;
43
43
  };
44
+ export declare type FunctionFieldNode = {
45
+ type: 'functionField';
46
+ name: string;
47
+ fieldKey: string;
48
+ query: Query;
49
+ relatedCollection: string;
50
+ };
44
51
  export declare type AST = {
45
52
  type: 'root';
46
53
  name: string;
47
- children: (NestedCollectionNode | FieldNode)[];
54
+ children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[];
48
55
  query: Query;
49
56
  };
@@ -129,8 +129,8 @@ function applySort(knex, schema, rootQuery, rootSort, collection, subQuery = fal
129
129
  relations,
130
130
  knex,
131
131
  });
132
- const colPath = (0, get_column_path_1.getColumnPath)({ path: column, collection, aliasMap, relations }) || '';
133
- const [alias, field] = colPath.split('.');
132
+ const { columnPath } = (0, get_column_path_1.getColumnPath)({ path: column, collection, aliasMap, relations });
133
+ const [alias, field] = columnPath.split('.');
134
134
  return {
135
135
  order,
136
136
  column: (0, get_column_1.getColumn)(knex, alias, field, false, schema),
@@ -208,18 +208,10 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
208
208
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
209
209
  if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
210
210
  if (filterPath.length > 1) {
211
- const columnName = (0, get_column_path_1.getColumnPath)({ path: filterPath, collection, relations, aliasMap });
212
- if (!columnName)
211
+ const { columnPath, targetCollection } = (0, get_column_path_1.getColumnPath)({ path: filterPath, collection, relations, aliasMap });
212
+ if (!columnPath)
213
213
  continue;
214
- if (relation === null || relation === void 0 ? void 0 : relation.related_collection) {
215
- applyFilterToQuery(columnName, filterOperator, filterValue, logical, relation.related_collection); // m2o
216
- }
217
- else if (filterPath[0].includes(':')) {
218
- applyFilterToQuery(columnName, filterOperator, filterValue, logical, filterPath[0].split(':')[1]); // a2o
219
- }
220
- else {
221
- applyFilterToQuery(columnName, filterOperator, filterValue, logical);
222
- }
214
+ applyFilterToQuery(columnPath, filterOperator, filterValue, logical, targetCollection);
223
215
  }
224
216
  else {
225
217
  applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue, logical);
@@ -32,23 +32,18 @@ async function applySnapshot(snapshot, options) {
32
32
  .filter((fieldDiff) => fieldDiff.collection === collection)
33
33
  .map((fieldDiff) => fieldDiff.diff[0].rhs)
34
34
  .map((fieldDiff) => {
35
- // Casts field type to UUID when applying SQLite-based schema on other databases.
36
- // This is needed because SQLite snapshots UUID fields as char with length 36, and
37
- // it will fail when trying to create relation between char field to UUID field
38
- if (!fieldDiff.schema ||
39
- fieldDiff.schema.data_type !== 'char' ||
40
- fieldDiff.schema.max_length !== 36 ||
41
- !fieldDiff.schema.foreign_key_table ||
42
- !fieldDiff.schema.foreign_key_column) {
43
- return fieldDiff;
35
+ var _a, _b, _c, _d, _e;
36
+ // Casts field type to UUID when applying non-PostgreSQL schema onto PostgreSQL database.
37
+ // This is needed because they snapshots UUID fields as char with length 36.
38
+ if (((_a = fieldDiff.schema) === null || _a === void 0 ? void 0 : _a.data_type) === 'char' &&
39
+ ((_b = fieldDiff.schema) === null || _b === void 0 ? void 0 : _b.max_length) === 36 &&
40
+ (((_c = fieldDiff.schema) === null || _c === void 0 ? void 0 : _c.is_primary_key) ||
41
+ (((_d = fieldDiff.schema) === null || _d === void 0 ? void 0 : _d.foreign_key_table) && ((_e = fieldDiff.schema) === null || _e === void 0 ? void 0 : _e.foreign_key_column)))) {
42
+ return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
44
43
  }
45
- const matchingForeignKeyTable = schema.collections[fieldDiff.schema.foreign_key_table];
46
- if (!matchingForeignKeyTable)
47
- return fieldDiff;
48
- const matchingForeignKeyField = matchingForeignKeyTable.fields[fieldDiff.schema.foreign_key_column];
49
- if (!matchingForeignKeyField || matchingForeignKeyField.type !== 'uuid')
44
+ else {
50
45
  return fieldDiff;
51
- return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
46
+ }
52
47
  });
53
48
  try {
54
49
  await collectionsService.createOne({
@@ -80,8 +75,37 @@ async function applySnapshot(snapshot, options) {
80
75
  }
81
76
  }
82
77
  };
83
- // create top level collections (no group) first, then continue with nested collections recursively
84
- await createCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === 'N' && ((_a = diff[0].rhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
78
+ // Finds all collections that need to be created
79
+ const filterCollectionsForCreation = ({ diff }) => {
80
+ var _a;
81
+ // Check new collections only
82
+ const isNewCollection = diff[0].kind === 'N';
83
+ if (!isNewCollection)
84
+ return false;
85
+ // Create now if no group
86
+ const groupName = (_a = diff[0].rhs.meta) === null || _a === void 0 ? void 0 : _a.group;
87
+ if (!groupName)
88
+ return true;
89
+ // Check if parent collection already exists in schema
90
+ const parentExists = current.collections.find((c) => c.collection === groupName) !== undefined;
91
+ // If this is a new collection and the parent collection doesn't exist in current schema ->
92
+ // Check if the parent collection will be created as part of applying this snapshot ->
93
+ // If yes -> this collection will be created recursively
94
+ // If not -> create now
95
+ // (ex.)
96
+ // TopLevelCollection - I exist in current schema
97
+ // NestedCollection - I exist in snapshotDiff as a new collection
98
+ // TheCurrentCollectionInIteration - I exist in snapshotDiff as a new collection but will be created as part of NestedCollection
99
+ const parentWillBeCreatedInThisApply = snapshotDiff.collections.filter(({ collection, diff }) => diff[0].kind === 'N' && collection === groupName)
100
+ .length > 0;
101
+ // Has group, but parent is not new, parent is also not being created in this snapshot apply
102
+ if (parentExists && !parentWillBeCreatedInThisApply)
103
+ return true;
104
+ return false;
105
+ };
106
+ // Create top level collections (no group, or highest level in existing group) first,
107
+ // then continue with nested collections recursively
108
+ await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
85
109
  // delete top level collections (no group) first, then continue with nested collections recursively
86
110
  await deleteCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === 'D' && ((_a = diff[0].lhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
87
111
  for (const { collection, diff } of snapshotDiff.collections) {
@@ -1,10 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.filterItems = void 0;
7
- const generate_joi_1 = __importDefault(require("./generate-joi"));
4
+ const utils_1 = require("@directus/shared/utils");
8
5
  /*
9
6
  Note: Filtering is normally done through SQL in run-ast. This function can be used in case an already
10
7
  existing array of items has to be filtered using the same filter syntax as used in the ast-to-sql flow
@@ -16,7 +13,7 @@ function filterItems(items, filter) {
16
13
  return passesFilter(item, filter);
17
14
  });
18
15
  function passesFilter(item, filter) {
19
- if (!filter)
16
+ if (!filter || Object.keys(filter).length === 0)
20
17
  return true;
21
18
  if (Object.keys(filter)[0] === '_and') {
22
19
  const subfilter = Object.values(filter)[0];
@@ -31,7 +28,7 @@ function filterItems(items, filter) {
31
28
  });
32
29
  }
33
30
  else {
34
- const schema = (0, generate_joi_1.default)(filter);
31
+ const schema = (0, utils_1.generateJoi)(filter);
35
32
  const { error } = schema.validate(item);
36
33
  return error === undefined;
37
34
  }
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const lodash_1 = require("lodash");
7
7
  const get_relation_type_1 = require("../utils/get-relation-type");
8
+ const constants_1 = require("@directus/shared/constants");
8
9
  async function getASTFromQuery(collection, query, schema, options) {
9
10
  var _a, _b, _c, _d, _e;
10
11
  query = (0, lodash_1.cloneDeep)(query);
@@ -118,6 +119,23 @@ async function getASTFromQuery(collection, query, schema, options) {
118
119
  }
119
120
  }
120
121
  else {
122
+ if (fieldKey.includes('(') && fieldKey.includes(')')) {
123
+ const columnName = fieldKey.match(constants_1.REGEX_BETWEEN_PARENS)[1];
124
+ const foundField = schema.collections[parentCollection].fields[columnName];
125
+ if (foundField && foundField.type === 'alias') {
126
+ const foundRelation = schema.relations.find((relation) => { var _a; return relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === columnName; });
127
+ if (foundRelation) {
128
+ children.push({
129
+ type: 'functionField',
130
+ name,
131
+ fieldKey,
132
+ query: {},
133
+ relatedCollection: foundRelation.collection,
134
+ });
135
+ continue;
136
+ }
137
+ }
138
+ }
121
139
  children.push({ type: 'field', name, fieldKey });
122
140
  }
123
141
  }