directus 9.16.0 → 9.17.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 (46) hide show
  1. package/dist/app.js +7 -7
  2. package/dist/auth/drivers/ldap.js +1 -0
  3. package/dist/auth/drivers/local.js +6 -0
  4. package/dist/auth/drivers/oauth2.js +1 -0
  5. package/dist/auth/drivers/openid.js +1 -0
  6. package/dist/cli/utils/create-env/env-stub.liquid +8 -1
  7. package/dist/controllers/activity.js +1 -0
  8. package/dist/controllers/auth.js +6 -9
  9. package/dist/database/helpers/schema/dialects/sqlite.d.ts +2 -2
  10. package/dist/database/helpers/schema/dialects/sqlite.js +2 -2
  11. package/dist/database/helpers/schema/types.d.ts +2 -2
  12. package/dist/database/helpers/schema/types.js +2 -2
  13. package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
  14. package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
  15. package/dist/database/system-data/fields/activity.yaml +6 -0
  16. package/dist/database/system-data/fields/sessions.yaml +2 -0
  17. package/dist/env.js +8 -0
  18. package/dist/extensions.d.ts +1 -0
  19. package/dist/extensions.js +16 -3
  20. package/dist/flows.js +27 -17
  21. package/dist/mailer.js +7 -0
  22. package/dist/middleware/authenticate.d.ts +1 -1
  23. package/dist/middleware/authenticate.js +1 -0
  24. package/dist/middleware/authenticate.test.js +44 -4
  25. package/dist/middleware/validate-batch.d.ts +1 -2
  26. package/dist/middleware/validate-batch.js +10 -13
  27. package/dist/middleware/validate-batch.test.d.ts +1 -0
  28. package/dist/middleware/validate-batch.test.js +82 -0
  29. package/dist/operations/request/index.js +2 -2
  30. package/dist/operations/transform/index.d.ts +1 -1
  31. package/dist/operations/transform/index.js +1 -1
  32. package/dist/services/authentication.js +13 -3
  33. package/dist/services/fields.js +11 -3
  34. package/dist/services/files.js +2 -2
  35. package/dist/services/graphql/index.js +45 -37
  36. package/dist/services/items.js +15 -3
  37. package/dist/services/payload.js +28 -4
  38. package/dist/services/relations.d.ts +2 -0
  39. package/dist/services/relations.js +14 -0
  40. package/dist/services/server.js +5 -4
  41. package/dist/services/shares.js +2 -1
  42. package/dist/utils/async-handler.d.ts +2 -6
  43. package/dist/utils/async-handler.js +1 -13
  44. package/dist/utils/async-handler.test.d.ts +1 -0
  45. package/dist/utils/async-handler.test.js +18 -0
  46. package/package.json +14 -11
@@ -14,39 +14,36 @@ const validateBatch = (scope) => (0, async_handler_1.default)(async (req, res, n
14
14
  req.body = {};
15
15
  return next();
16
16
  }
17
+ if (req.method.toLowerCase() !== 'search' && scope !== 'read' && req.singleton) {
18
+ return next();
19
+ }
17
20
  if (!req.body)
18
21
  throw new exceptions_1.InvalidPayloadException('Payload in body is required');
19
- if (req.singleton)
22
+ if (['update', 'delete'].includes(scope) && Array.isArray(req.body)) {
20
23
  return next();
24
+ }
25
+ // In reads, the query in the body should override the query params for searching
26
+ if (scope === 'read' && req.body.query) {
27
+ req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
28
+ }
21
29
  // Every cRUD action has either keys or query
22
30
  let batchSchema = joi_1.default.object().keys({
23
31
  keys: joi_1.default.array().items(joi_1.default.alternatives(joi_1.default.string(), joi_1.default.number())),
24
32
  query: joi_1.default.object().unknown(),
25
33
  });
26
- // In reads, you can't combine the two, and 1 of the two at least is required
27
- if (scope !== 'read') {
34
+ if (['update', 'delete'].includes(scope)) {
28
35
  batchSchema = batchSchema.xor('query', 'keys');
29
36
  }
30
37
  // In updates, we add a required `data` that holds the update payload if an array isn't used
31
38
  if (scope === 'update') {
32
- if (Array.isArray(req.body))
33
- return next();
34
39
  batchSchema = batchSchema.keys({
35
40
  data: joi_1.default.object().unknown().required(),
36
41
  });
37
42
  }
38
- // In deletes, we want to keep supporting an array of just primary keys
39
- if (scope === 'delete' && Array.isArray(req.body)) {
40
- return next();
41
- }
42
43
  const { error } = batchSchema.validate(req.body);
43
44
  if (error) {
44
45
  throw new exceptions_2.FailedValidationException(error.details[0]);
45
46
  }
46
- // In reads, the query in the body should override the query params for searching
47
- if (scope === 'read' && req.body.query) {
48
- req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
49
- }
50
47
  return next();
51
48
  });
52
49
  exports.validateBatch = validateBatch;
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const validate_batch_1 = require("./validate-batch");
4
+ require("../../src/types/express.d.ts");
5
+ const exceptions_1 = require("../exceptions");
6
+ const exceptions_2 = require("@directus/shared/exceptions");
7
+ let mockRequest;
8
+ let mockResponse;
9
+ const nextFunction = jest.fn();
10
+ beforeEach(() => {
11
+ mockRequest = {};
12
+ mockResponse = {};
13
+ jest.clearAllMocks();
14
+ });
15
+ test('Sets body to empty, calls next on GET requests', async () => {
16
+ mockRequest.method = 'GET';
17
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
18
+ expect(mockRequest.body).toEqual({});
19
+ expect(nextFunction).toHaveBeenCalledTimes(1);
20
+ });
21
+ test(`Short circuits on singletons that aren't queried through SEARCH`, async () => {
22
+ mockRequest.method = 'PATCH';
23
+ mockRequest.singleton = true;
24
+ mockRequest.body = { title: 'test' };
25
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
26
+ expect(nextFunction).toHaveBeenCalledTimes(1);
27
+ });
28
+ test('Throws InvalidPayloadException on missing body', async () => {
29
+ mockRequest.method = 'SEARCH';
30
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
31
+ expect(nextFunction).toHaveBeenCalledTimes(1);
32
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_1.InvalidPayloadException);
33
+ });
34
+ test(`Short circuits on Array body in update/delete use`, async () => {
35
+ mockRequest.method = 'PATCH';
36
+ mockRequest.body = [1, 2, 3];
37
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
38
+ expect(mockRequest.sanitizedQuery).toBe(undefined);
39
+ expect(nextFunction).toHaveBeenCalled();
40
+ });
41
+ test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
42
+ mockRequest.method = 'SEARCH';
43
+ mockRequest.body = {
44
+ query: {
45
+ sort: 'id',
46
+ },
47
+ };
48
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
49
+ expect(mockRequest.sanitizedQuery).toEqual({
50
+ sort: ['id'],
51
+ });
52
+ });
53
+ test(`Doesn't allow both query and keys in a batch delete`, async () => {
54
+ mockRequest.method = 'DELETE';
55
+ mockRequest.body = {
56
+ keys: [1, 2, 3],
57
+ query: { filter: {} },
58
+ };
59
+ await (0, validate_batch_1.validateBatch)('delete')(mockRequest, mockResponse, nextFunction);
60
+ expect(nextFunction).toHaveBeenCalledTimes(1);
61
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
62
+ });
63
+ test(`Requires 'data' on batch update`, async () => {
64
+ mockRequest.method = 'PATCH';
65
+ mockRequest.body = {
66
+ keys: [1, 2, 3],
67
+ query: { filter: {} },
68
+ };
69
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
70
+ expect(nextFunction).toHaveBeenCalledTimes(1);
71
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
72
+ });
73
+ test(`Calls next when all is well`, async () => {
74
+ mockRequest.method = 'PATCH';
75
+ mockRequest.body = {
76
+ query: { filter: {} },
77
+ data: {},
78
+ };
79
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
80
+ expect(nextFunction).toHaveBeenCalledTimes(1);
81
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeUndefined();
82
+ });
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const utils_1 = require("@directus/shared/utils");
7
7
  const axios_1 = __importDefault(require("axios"));
8
+ const encodeurl_1 = __importDefault(require("encodeurl"));
8
9
  exports.default = (0, utils_1.defineOperationApi)({
9
10
  id: 'request',
10
11
  handler: async ({ url, method, body, headers }) => {
@@ -16,9 +17,8 @@ exports.default = (0, utils_1.defineOperationApi)({
16
17
  if (!customHeaders['Content-Type'] && isValidJSON(body)) {
17
18
  customHeaders['Content-Type'] = 'application/json';
18
19
  }
19
- const shouldEncode = decodeURI(url) === url;
20
20
  const result = await (0, axios_1.default)({
21
- url: shouldEncode ? encodeURI(url) : url,
21
+ url: (0, encodeurl_1.default)(url),
22
22
  method,
23
23
  data: body,
24
24
  headers: customHeaders,
@@ -1,5 +1,5 @@
1
1
  declare type Options = {
2
- json: string;
2
+ json: string | Record<string, any>;
3
3
  };
4
4
  declare const _default: import("@directus/shared/types").OperationApiConfig<Options>;
5
5
  export default _default;
@@ -4,6 +4,6 @@ const utils_1 = require("@directus/shared/utils");
4
4
  exports.default = (0, utils_1.defineOperationApi)({
5
5
  id: 'transform',
6
6
  handler: ({ json }) => {
7
- return (0, utils_1.parseJSON)(json);
7
+ return (0, utils_1.optionToObject)(json);
8
8
  },
9
9
  });
@@ -36,15 +36,23 @@ class AuthenticationService {
36
36
  * to handle password existence checks elsewhere
37
37
  */
38
38
  async login(providerName = constants_1.DEFAULT_AUTH_PROVIDER, payload, otp) {
39
- var _a, _b;
40
- const STALL_TIME = 100;
39
+ var _a, _b, _c;
40
+ const STALL_TIME = env_1.default.LOGIN_STALL_TIME;
41
41
  const timeStart = perf_hooks_1.performance.now();
42
42
  const provider = (0, auth_1.getAuthProvider)(providerName);
43
+ let userId;
44
+ try {
45
+ userId = await provider.getUserID((0, lodash_1.cloneDeep)(payload));
46
+ }
47
+ catch (err) {
48
+ await (0, stall_1.stall)(STALL_TIME, timeStart);
49
+ throw err;
50
+ }
43
51
  const user = await this.knex
44
52
  .select('u.id', 'u.first_name', 'u.last_name', 'u.email', 'u.password', 'u.status', 'u.role', 'r.admin_access', 'r.app_access', 'u.tfa_secret', 'u.provider', 'u.external_identifier', 'u.auth_data')
45
53
  .from('directus_users as u')
46
54
  .leftJoin('directus_roles as r', 'u.role', 'r.id')
47
- .where('u.id', await provider.getUserID((0, lodash_1.cloneDeep)(payload)))
55
+ .where('u.id', userId)
48
56
  .first();
49
57
  const updatedPayload = await emitter_1.default.emitFilter('auth.login', payload, {
50
58
  status: 'pending',
@@ -151,6 +159,7 @@ class AuthenticationService {
151
159
  expires: refreshTokenExpiration,
152
160
  ip: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.ip,
153
161
  user_agent: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.userAgent,
162
+ origin: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.origin,
154
163
  });
155
164
  await this.knex('directus_sessions').delete().where('expires', '<', new Date());
156
165
  if (this.accountability) {
@@ -159,6 +168,7 @@ class AuthenticationService {
159
168
  user: user.id,
160
169
  ip: this.accountability.ip,
161
170
  user_agent: this.accountability.userAgent,
171
+ origin: this.accountability.origin,
162
172
  collection: 'directus_users',
163
173
  item: user.id,
164
174
  });
@@ -222,6 +222,7 @@ class FieldsService {
222
222
  if (this.accountability && this.accountability.admin !== true) {
223
223
  throw new exceptions_1.ForbiddenException();
224
224
  }
225
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
225
226
  try {
226
227
  const exists = field.field in this.schema.collections[collection].fields ||
227
228
  (0, lodash_1.isNil)(await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()) === false;
@@ -276,6 +277,9 @@ class FieldsService {
276
277
  });
277
278
  }
278
279
  finally {
280
+ if (runPostColumnChange) {
281
+ await this.helpers.schema.postColumnChange();
282
+ }
279
283
  if (this.cache && env_1.default.CACHE_AUTO_PURGE) {
280
284
  await this.cache.clear();
281
285
  }
@@ -286,6 +290,7 @@ class FieldsService {
286
290
  if (this.accountability && this.accountability.admin !== true) {
287
291
  throw new exceptions_1.ForbiddenException();
288
292
  }
293
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
289
294
  try {
290
295
  const hookAdjustedField = await emitter_1.default.emitFilter(`fields.update`, field, {
291
296
  keys: [field.field],
@@ -341,6 +346,9 @@ class FieldsService {
341
346
  return field.field;
342
347
  }
343
348
  finally {
349
+ if (runPostColumnChange) {
350
+ await this.helpers.schema.postColumnChange();
351
+ }
344
352
  if (this.cache && env_1.default.CACHE_AUTO_PURGE) {
345
353
  await this.cache.clear();
346
354
  }
@@ -351,7 +359,7 @@ class FieldsService {
351
359
  if (this.accountability && this.accountability.admin !== true) {
352
360
  throw new exceptions_1.ForbiddenException();
353
361
  }
354
- const runPostColumnDelete = await this.helpers.schema.preColumnDelete();
362
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
355
363
  try {
356
364
  await emitter_1.default.emitFilter('fields.delete', [field], {
357
365
  collection: collection,
@@ -440,8 +448,8 @@ class FieldsService {
440
448
  });
441
449
  }
442
450
  finally {
443
- if (runPostColumnDelete) {
444
- await this.helpers.schema.postColumnDelete();
451
+ if (runPostColumnChange) {
452
+ await this.helpers.schema.postColumnChange();
445
453
  }
446
454
  if (this.cache && env_1.default.CACHE_AUTO_PURGE) {
447
455
  await this.cache.clear();
@@ -46,6 +46,7 @@ const utils_1 = require("@directus/shared/utils");
46
46
  const items_1 = require("./items");
47
47
  const net_1 = __importDefault(require("net"));
48
48
  const os_1 = __importDefault(require("os"));
49
+ const encodeurl_1 = __importDefault(require("encodeurl"));
49
50
  const lookupDNS = (0, util_1.promisify)(dns_1.lookup);
50
51
  class FilesService extends items_1.ItemsService {
51
52
  constructor(options) {
@@ -229,8 +230,7 @@ class FilesService extends items_1.ItemsService {
229
230
  }
230
231
  let fileResponse;
231
232
  try {
232
- const shouldEncode = decodeURI(importURL) === importURL;
233
- fileResponse = await axios_1.default.get(shouldEncode ? encodeURI(importURL) : importURL, {
233
+ fileResponse = await axios_1.default.get((0, encodeurl_1.default)(importURL), {
234
234
  responseType: 'stream',
235
235
  });
236
236
  }
@@ -293,41 +293,43 @@ class GraphQLService {
293
293
  return obj[field.field];
294
294
  },
295
295
  };
296
- if (field.type === 'date') {
297
- acc[`${field.field}_func`] = {
298
- type: DateFunctions,
299
- resolve: (obj) => {
300
- const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
301
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
302
- },
303
- };
304
- }
305
- if (field.type === 'time') {
306
- acc[`${field.field}_func`] = {
307
- type: TimeFunctions,
308
- resolve: (obj) => {
309
- const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
310
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
311
- },
312
- };
313
- }
314
- if (field.type === 'dateTime' || field.type === 'timestamp') {
315
- acc[`${field.field}_func`] = {
316
- type: DateTimeFunctions,
317
- resolve: (obj) => {
318
- const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
319
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
320
- },
321
- };
322
- }
323
- if (field.type === 'json' || field.type === 'alias') {
324
- acc[`${field.field}_func`] = {
325
- type: CountFunctions,
326
- resolve: (obj) => {
327
- const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
328
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
329
- },
330
- };
296
+ if (action === 'read') {
297
+ if (field.type === 'date') {
298
+ acc[`${field.field}_func`] = {
299
+ type: DateFunctions,
300
+ resolve: (obj) => {
301
+ const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
302
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
303
+ },
304
+ };
305
+ }
306
+ if (field.type === 'time') {
307
+ acc[`${field.field}_func`] = {
308
+ type: TimeFunctions,
309
+ resolve: (obj) => {
310
+ const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
311
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
312
+ },
313
+ };
314
+ }
315
+ if (field.type === 'dateTime' || field.type === 'timestamp') {
316
+ acc[`${field.field}_func`] = {
317
+ type: DateTimeFunctions,
318
+ resolve: (obj) => {
319
+ const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
320
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
321
+ },
322
+ };
323
+ }
324
+ if (field.type === 'json' || field.type === 'alias') {
325
+ acc[`${field.field}_func`] = {
326
+ type: CountFunctions,
327
+ resolve: (obj) => {
328
+ const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
329
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
330
+ },
331
+ };
332
+ }
331
333
  }
332
334
  return acc;
333
335
  }, {}),
@@ -1615,6 +1617,7 @@ class GraphQLService {
1615
1617
  const accountability = {
1616
1618
  ip: req === null || req === void 0 ? void 0 : req.ip,
1617
1619
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1620
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1618
1621
  role: null,
1619
1622
  };
1620
1623
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1649,6 +1652,7 @@ class GraphQLService {
1649
1652
  const accountability = {
1650
1653
  ip: req === null || req === void 0 ? void 0 : req.ip,
1651
1654
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1655
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1652
1656
  role: null,
1653
1657
  };
1654
1658
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1685,6 +1689,7 @@ class GraphQLService {
1685
1689
  const accountability = {
1686
1690
  ip: req === null || req === void 0 ? void 0 : req.ip,
1687
1691
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1692
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1688
1693
  role: null,
1689
1694
  };
1690
1695
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1709,6 +1714,7 @@ class GraphQLService {
1709
1714
  const accountability = {
1710
1715
  ip: req === null || req === void 0 ? void 0 : req.ip,
1711
1716
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1717
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1712
1718
  role: null,
1713
1719
  };
1714
1720
  const service = new users_1.UsersService({ accountability, schema: this.schema });
@@ -1733,6 +1739,7 @@ class GraphQLService {
1733
1739
  const accountability = {
1734
1740
  ip: req === null || req === void 0 ? void 0 : req.ip,
1735
1741
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1742
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1736
1743
  role: null,
1737
1744
  };
1738
1745
  const service = new users_1.UsersService({ accountability, schema: this.schema });
@@ -2303,7 +2310,7 @@ class GraphQLService {
2303
2310
  comment: (0, graphql_1.GraphQLNonNull)(graphql_1.GraphQLString),
2304
2311
  },
2305
2312
  resolve: async (_, args, __, info) => {
2306
- var _a, _b, _c, _d, _e;
2313
+ var _a, _b, _c, _d, _e, _f;
2307
2314
  const service = new activity_1.ActivityService({
2308
2315
  accountability: this.accountability,
2309
2316
  schema: this.schema,
@@ -2314,9 +2321,10 @@ class GraphQLService {
2314
2321
  user: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.user,
2315
2322
  ip: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.ip,
2316
2323
  user_agent: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.userAgent,
2324
+ origin: (_d = this.accountability) === null || _d === void 0 ? void 0 : _d.origin,
2317
2325
  });
2318
2326
  if ('directus_activity' in ReadCollectionTypes) {
2319
- const selections = this.replaceFragmentsInSelections((_e = (_d = info.fieldNodes[0]) === null || _d === void 0 ? void 0 : _d.selectionSet) === null || _e === void 0 ? void 0 : _e.selections, info.fragments);
2327
+ const selections = this.replaceFragmentsInSelections((_f = (_e = info.fieldNodes[0]) === null || _e === void 0 ? void 0 : _e.selectionSet) === null || _f === void 0 ? void 0 : _f.selections, info.fragments);
2320
2328
  const query = this.getQuery(args, selections || [], info.variableValues);
2321
2329
  return await service.readOne(primaryKey, query);
2322
2330
  }
@@ -130,6 +130,7 @@ class ItemsService {
130
130
  collection: this.collection,
131
131
  ip: this.accountability.ip,
132
132
  user_agent: this.accountability.userAgent,
133
+ origin: this.accountability.origin,
133
134
  item: primaryKey,
134
135
  });
135
136
  // If revisions are tracked, create revisions record
@@ -214,7 +215,16 @@ class ItemsService {
214
215
  * Get items by query
215
216
  */
216
217
  async readByQuery(query, opts) {
217
- let ast = await (0, get_ast_from_query_1.default)(this.collection, query, this.schema, {
218
+ const updatedQuery = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
219
+ ? await emitter_1.default.emitFilter(`${this.eventScope}.query`, query, {
220
+ collection: this.collection,
221
+ }, {
222
+ database: this.knex,
223
+ schema: this.schema,
224
+ accountability: this.accountability,
225
+ })
226
+ : query;
227
+ let ast = await (0, get_ast_from_query_1.default)(this.collection, updatedQuery, this.schema, {
218
228
  accountability: this.accountability,
219
229
  // By setting the permissions action, you can read items using the permissions for another
220
230
  // operation's permissions. This is used to dynamically check if you have update/delete
@@ -240,7 +250,7 @@ class ItemsService {
240
250
  }
241
251
  const filteredRecords = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
242
252
  ? await emitter_1.default.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
243
- query,
253
+ query: updatedQuery,
244
254
  collection: this.collection,
245
255
  }, {
246
256
  database: this.knex,
@@ -251,7 +261,7 @@ class ItemsService {
251
261
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
252
262
  emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, {
253
263
  payload: filteredRecords,
254
- query,
264
+ query: updatedQuery,
255
265
  collection: this.collection,
256
266
  }, {
257
267
  database: this.knex || (0, database_1.default)(),
@@ -406,6 +416,7 @@ class ItemsService {
406
416
  collection: this.collection,
407
417
  ip: this.accountability.ip,
408
418
  user_agent: this.accountability.userAgent,
419
+ origin: this.accountability.origin,
409
420
  item: key,
410
421
  })));
411
422
  if (this.schema.collections[this.collection].accountability === 'all') {
@@ -573,6 +584,7 @@ class ItemsService {
573
584
  collection: this.collection,
574
585
  ip: this.accountability.ip,
575
586
  user_agent: this.accountability.userAgent,
587
+ origin: this.accountability.origin,
576
588
  item: key,
577
589
  })));
578
590
  }
@@ -538,10 +538,34 @@ class PayloadService {
538
538
  if (error)
539
539
  throw new exceptions_1.InvalidPayloadException(`Invalid one-to-many update structure: ${error.message}`);
540
540
  if (alterations.create) {
541
- await itemsService.createMany(alterations.create.map((item) => ({
542
- ...item,
543
- [relation.field]: parent || payload[currentPrimaryKeyField],
544
- })), {
541
+ const sortField = relation.meta.sort_field;
542
+ let createPayload;
543
+ if (sortField !== null) {
544
+ const highestOrderNumber = await this.knex
545
+ .from(relation.collection)
546
+ .where({ [relation.field]: parent })
547
+ .whereNotNull(sortField)
548
+ .max(sortField)
549
+ .first();
550
+ createPayload = alterations.create.map((item, index) => {
551
+ const record = (0, lodash_1.cloneDeep)(item);
552
+ // add sort field value if it is not supplied in the item
553
+ if (parent !== null && record[sortField] === undefined) {
554
+ record[sortField] = (highestOrderNumber === null || highestOrderNumber === void 0 ? void 0 : highestOrderNumber.max) ? highestOrderNumber.max + index + 1 : index + 1;
555
+ }
556
+ return {
557
+ ...record,
558
+ [relation.field]: parent || payload[currentPrimaryKeyField],
559
+ };
560
+ });
561
+ }
562
+ else {
563
+ createPayload = alterations.create.map((item) => ({
564
+ ...item,
565
+ [relation.field]: parent || payload[currentPrimaryKeyField],
566
+ }));
567
+ }
568
+ await itemsService.createMany(createPayload, {
545
569
  onRevisionCreate: (pk) => revisions.push(pk),
546
570
  bypassEmitAction: (params) => nestedActionEvents.push(params),
547
571
  emitEvents: opts === null || opts === void 0 ? void 0 : opts.emitEvents,
@@ -5,6 +5,7 @@ import { PermissionsService } from './permissions';
5
5
  import SchemaInspector from '@directus/schema';
6
6
  import Keyv from 'keyv';
7
7
  import { AbstractServiceOptions } from '../types';
8
+ import { Helpers } from '../database/helpers';
8
9
  export declare class RelationsService {
9
10
  knex: Knex;
10
11
  permissionsService: PermissionsService;
@@ -13,6 +14,7 @@ export declare class RelationsService {
13
14
  schema: SchemaOverview;
14
15
  relationsItemService: ItemsService<RelationMeta>;
15
16
  systemCache: Keyv<any>;
17
+ helpers: Helpers;
16
18
  constructor(options: AbstractServiceOptions);
17
19
  readAll(collection?: string, opts?: QueryOptions): Promise<Relation[]>;
18
20
  readOne(collection: string, field: string): Promise<Relation>;
@@ -36,6 +36,7 @@ const schema_1 = __importDefault(require("@directus/schema"));
36
36
  const database_1 = __importStar(require("../database"));
37
37
  const get_default_index_name_1 = require("../utils/get-default-index-name");
38
38
  const cache_1 = require("../cache");
39
+ const helpers_1 = require("../database/helpers");
39
40
  class RelationsService {
40
41
  constructor(options) {
41
42
  this.knex = options.knex || (0, database_1.default)();
@@ -51,6 +52,7 @@ class RelationsService {
51
52
  // happens in `filterForbidden` down below
52
53
  });
53
54
  this.systemCache = (0, cache_1.getCache)().systemCache;
55
+ this.helpers = (0, helpers_1.getHelpers)(this.knex);
54
56
  }
55
57
  async readAll(collection, opts) {
56
58
  if (this.accountability && this.accountability.admin !== true && this.hasReadAccess === false) {
@@ -150,6 +152,7 @@ class RelationsService {
150
152
  if (existingRelation) {
151
153
  throw new exceptions_1.InvalidPayloadException(`Field "${relation.field}" in collection "${relation.collection}" already has an associated relationship`);
152
154
  }
155
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
153
156
  try {
154
157
  const metaRow = {
155
158
  ...(relation.meta || {}),
@@ -182,6 +185,9 @@ class RelationsService {
182
185
  });
183
186
  }
184
187
  finally {
188
+ if (runPostColumnChange) {
189
+ await this.helpers.schema.postColumnChange();
190
+ }
185
191
  await (0, cache_1.clearSystemCache)();
186
192
  }
187
193
  }
@@ -204,6 +210,7 @@ class RelationsService {
204
210
  if (!existingRelation) {
205
211
  throw new exceptions_1.InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`);
206
212
  }
213
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
207
214
  try {
208
215
  await this.knex.transaction(async (trx) => {
209
216
  if (existingRelation.related_collection) {
@@ -247,6 +254,9 @@ class RelationsService {
247
254
  });
248
255
  }
249
256
  finally {
257
+ if (runPostColumnChange) {
258
+ await this.helpers.schema.postColumnChange();
259
+ }
250
260
  await (0, cache_1.clearSystemCache)();
251
261
  }
252
262
  }
@@ -267,6 +277,7 @@ class RelationsService {
267
277
  if (!existingRelation) {
268
278
  throw new exceptions_1.InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`);
269
279
  }
280
+ const runPostColumnChange = await this.helpers.schema.preColumnChange();
270
281
  try {
271
282
  await this.knex.transaction(async (trx) => {
272
283
  var _a;
@@ -284,6 +295,9 @@ class RelationsService {
284
295
  });
285
296
  }
286
297
  finally {
298
+ if (runPostColumnChange) {
299
+ await this.helpers.schema.postColumnChange();
300
+ }
287
301
  await (0, cache_1.clearSystemCache)();
288
302
  }
289
303
  }