directus 9.15.1 → 9.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/__utils__/items-utils.d.ts +2 -0
  2. package/dist/__utils__/items-utils.js +36 -0
  3. package/dist/__utils__/schemas.d.ts +13 -0
  4. package/dist/__utils__/schemas.js +304 -0
  5. package/dist/__utils__/snapshots.d.ts +5 -0
  6. package/dist/__utils__/snapshots.js +897 -0
  7. package/dist/app.js +1 -0
  8. package/dist/cli/index.test.d.ts +1 -0
  9. package/dist/cli/index.test.js +58 -0
  10. package/dist/cli/utils/create-env/env-stub.liquid +2 -2
  11. package/dist/controllers/assets.js +20 -16
  12. package/dist/controllers/files.test.d.ts +1 -0
  13. package/dist/controllers/files.test.js +49 -0
  14. package/dist/controllers/server.js +0 -1
  15. package/dist/database/migrations/run.test.d.ts +1 -0
  16. package/dist/database/migrations/run.test.js +92 -0
  17. package/dist/env.js +8 -0
  18. package/dist/env.test.d.ts +8 -0
  19. package/dist/env.test.js +39 -0
  20. package/dist/flows.js +2 -1
  21. package/dist/middleware/authenticate.test.d.ts +1 -0
  22. package/dist/middleware/authenticate.test.js +174 -0
  23. package/dist/middleware/extract-token.test.d.ts +1 -0
  24. package/dist/middleware/extract-token.test.js +60 -0
  25. package/dist/operations/exec/index.d.ts +5 -0
  26. package/dist/operations/exec/index.js +26 -0
  27. package/dist/operations/exec/index.test.d.ts +1 -0
  28. package/dist/operations/exec/index.test.js +95 -0
  29. package/dist/operations/notification/index.js +9 -6
  30. package/dist/operations/request/index.js +22 -3
  31. package/dist/services/files.js +3 -2
  32. package/dist/services/files.test.d.ts +1 -0
  33. package/dist/services/files.test.js +53 -0
  34. package/dist/services/flows.js +4 -0
  35. package/dist/services/graphql/index.d.ts +2 -2
  36. package/dist/services/graphql/index.js +33 -38
  37. package/dist/services/items.js +83 -39
  38. package/dist/services/items.test.d.ts +1 -0
  39. package/dist/services/items.test.js +765 -0
  40. package/dist/services/payload.d.ts +7 -4
  41. package/dist/services/payload.js +35 -8
  42. package/dist/services/payload.test.d.ts +1 -0
  43. package/dist/services/payload.test.js +94 -0
  44. package/dist/services/server.js +5 -3
  45. package/dist/services/specifications.test.d.ts +1 -0
  46. package/dist/services/specifications.test.js +96 -0
  47. package/dist/types/items.d.ts +11 -0
  48. package/dist/utils/apply-query.js +7 -3
  49. package/dist/utils/apply-snapshot.js +15 -0
  50. package/dist/utils/apply-snapshot.test.d.ts +1 -0
  51. package/dist/utils/apply-snapshot.test.js +305 -0
  52. package/dist/utils/calculate-field-depth.test.d.ts +1 -0
  53. package/dist/utils/calculate-field-depth.test.js +76 -0
  54. package/dist/utils/filter-items.test.d.ts +1 -0
  55. package/dist/utils/filter-items.test.js +60 -0
  56. package/dist/utils/get-cache-key.test.d.ts +1 -0
  57. package/dist/utils/get-cache-key.test.js +53 -0
  58. package/dist/utils/get-column-path.test.d.ts +1 -0
  59. package/dist/utils/get-column-path.test.js +136 -0
  60. package/dist/utils/get-config-from-env.test.d.ts +1 -0
  61. package/dist/utils/get-config-from-env.test.js +19 -0
  62. package/dist/utils/get-graphql-type.d.ts +1 -1
  63. package/dist/utils/get-graphql-type.js +4 -1
  64. package/dist/utils/get-os-info.d.ts +9 -0
  65. package/dist/utils/get-os-info.js +47 -0
  66. package/dist/utils/get-relation-info.test.d.ts +1 -0
  67. package/dist/utils/get-relation-info.test.js +88 -0
  68. package/dist/utils/get-relation-type.test.d.ts +1 -0
  69. package/dist/utils/get-relation-type.test.js +69 -0
  70. package/dist/utils/get-string-byte-size.test.d.ts +1 -0
  71. package/dist/utils/get-string-byte-size.test.js +8 -0
  72. package/dist/utils/is-directus-jwt.test.d.ts +1 -0
  73. package/dist/utils/is-directus-jwt.test.js +26 -0
  74. package/dist/utils/jwt.test.d.ts +1 -0
  75. package/dist/utils/jwt.test.js +36 -0
  76. package/dist/utils/merge-permissions.test.d.ts +1 -0
  77. package/dist/utils/merge-permissions.test.js +80 -0
  78. package/dist/utils/validate-keys.test.d.ts +1 -0
  79. package/dist/utils/validate-keys.test.js +97 -0
  80. package/package.json +10 -10
@@ -0,0 +1,95 @@
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
+ const vm2_1 = require("vm2");
7
+ const index_1 = __importDefault(require("./index"));
8
+ test('Rejects when modules are used without modules being allowed', async () => {
9
+ const testCode = `
10
+ const test = require('test');
11
+ `;
12
+ await expect(index_1.default.handler({ code: testCode }, {
13
+ data: {},
14
+ env: {
15
+ FLOWS_EXEC_ALLOWED_MODULES: '',
16
+ },
17
+ })).rejects.toEqual(new vm2_1.VMError("Cannot find module 'test'"));
18
+ });
19
+ test('Rejects when code contains syntax errors', async () => {
20
+ const testCode = `
21
+ ~~
22
+ `;
23
+ await expect(index_1.default.handler({ code: testCode }, {
24
+ data: {},
25
+ env: {
26
+ FLOWS_EXEC_ALLOWED_MODULES: '',
27
+ },
28
+ })).rejects.toEqual(new Error("Couldn't compile code: Unexpected end of input"));
29
+ });
30
+ test('Rejects when returned function does something illegal', async () => {
31
+ const testCode = `
32
+ module.exports = function() {
33
+ return a + b;
34
+ };
35
+ `;
36
+ await expect(index_1.default.handler({ code: testCode }, {
37
+ data: {},
38
+ env: {
39
+ FLOWS_EXEC_ALLOWED_MODULES: '',
40
+ },
41
+ })).rejects.toEqual(new ReferenceError('a is not defined'));
42
+ });
43
+ test("Rejects when code doesn't return valid function", async () => {
44
+ const testCode = `
45
+ module.exports = false;
46
+ `;
47
+ await expect(index_1.default.handler({ code: testCode }, {
48
+ data: {},
49
+ env: {
50
+ FLOWS_EXEC_ALLOWED_MODULES: '',
51
+ },
52
+ })).rejects.toEqual(new TypeError('fn is not a function'));
53
+ });
54
+ test('Rejects returned function throws errors', async () => {
55
+ const testCode = `
56
+ module.exports = function () {
57
+ throw new Error('test');
58
+ };
59
+ `;
60
+ await expect(index_1.default.handler({ code: testCode }, {
61
+ data: {},
62
+ env: {
63
+ FLOWS_EXEC_ALLOWED_MODULES: '',
64
+ },
65
+ })).rejects.toEqual(new Error('test'));
66
+ });
67
+ test('Executes function when valid', () => {
68
+ const testCode = `
69
+ module.exports = function (data) {
70
+ return { result: data.input + ' test' };
71
+ };
72
+ `;
73
+ expect(index_1.default.handler({ code: testCode }, {
74
+ data: {
75
+ input: 'start',
76
+ },
77
+ env: {
78
+ FLOWS_EXEC_ALLOWED_MODULES: '',
79
+ },
80
+ })).resolves.toEqual({ result: 'start test' });
81
+ });
82
+ test('Allows modules that are whitelisted', () => {
83
+ const testCode = `
84
+ const bytes = require('bytes');
85
+
86
+ module.exports = function (data) {
87
+ return { result: bytes(1000) };
88
+ };
89
+ `;
90
+ expect(index_1.default.handler({ code: testCode }, {
91
+ env: {
92
+ FLOWS_EXEC_ALLOWED_MODULES: 'bytes',
93
+ },
94
+ })).resolves.toEqual({ result: '1000B' });
95
+ });
@@ -6,7 +6,6 @@ const get_accountability_for_role_1 = require("../../utils/get-accountability-fo
6
6
  exports.default = (0, utils_1.defineOperationApi)({
7
7
  id: 'notification',
8
8
  handler: async ({ recipient, subject, message, permissions }, { accountability, database, getSchema }) => {
9
- var _a;
10
9
  const schema = await getSchema({ database });
11
10
  let customAccountability;
12
11
  if (!permissions || permissions === '$trigger') {
@@ -27,12 +26,16 @@ exports.default = (0, utils_1.defineOperationApi)({
27
26
  knex: database,
28
27
  });
29
28
  const messageString = message ? (0, utils_1.optionToString)(message) : null;
30
- const result = await notificationsService.createOne({
31
- recipient,
32
- sender: (_a = customAccountability === null || customAccountability === void 0 ? void 0 : customAccountability.user) !== null && _a !== void 0 ? _a : null,
33
- subject,
34
- message: messageString,
29
+ const payload = (0, utils_1.toArray)(recipient).map((userId) => {
30
+ var _a;
31
+ return {
32
+ recipient: userId,
33
+ sender: (_a = customAccountability === null || customAccountability === void 0 ? void 0 : customAccountability.user) !== null && _a !== void 0 ? _a : null,
34
+ subject,
35
+ message: messageString,
36
+ };
35
37
  });
38
+ const result = await notificationsService.createMany(payload);
36
39
  return result;
37
40
  },
38
41
  });
@@ -8,11 +8,30 @@ const axios_1 = __importDefault(require("axios"));
8
8
  exports.default = (0, utils_1.defineOperationApi)({
9
9
  id: 'request',
10
10
  handler: async ({ url, method, body, headers }) => {
11
- const customHeaders = headers === null || headers === void 0 ? void 0 : headers.reduce((acc, { header, value }) => {
11
+ var _a;
12
+ const customHeaders = (_a = headers === null || headers === void 0 ? void 0 : headers.reduce((acc, { header, value }) => {
12
13
  acc[header] = value;
13
14
  return acc;
14
- }, {});
15
- const result = await (0, axios_1.default)({ url: encodeURI(url), method, data: body, headers: customHeaders });
15
+ }, {})) !== null && _a !== void 0 ? _a : {};
16
+ if (!customHeaders['Content-Type'] && isValidJSON(body)) {
17
+ customHeaders['Content-Type'] = 'application/json';
18
+ }
19
+ const shouldEncode = decodeURI(url) === url;
20
+ const result = await (0, axios_1.default)({
21
+ url: shouldEncode ? encodeURI(url) : url,
22
+ method,
23
+ data: body,
24
+ headers: customHeaders,
25
+ });
16
26
  return { status: result.status, statusText: result.statusText, headers: result.headers, data: result.data };
27
+ function isValidJSON(value) {
28
+ try {
29
+ (0, utils_1.parseJSON)(value);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
17
36
  },
18
37
  });
@@ -229,7 +229,8 @@ class FilesService extends items_1.ItemsService {
229
229
  }
230
230
  let fileResponse;
231
231
  try {
232
- fileResponse = await axios_1.default.get(importURL, {
232
+ const shouldEncode = decodeURI(importURL) === importURL;
233
+ fileResponse = await axios_1.default.get(shouldEncode ? encodeURI(importURL) : importURL, {
233
234
  responseType: 'stream',
234
235
  });
235
236
  }
@@ -240,7 +241,7 @@ class FilesService extends items_1.ItemsService {
240
241
  });
241
242
  }
242
243
  const parsedURL = url_1.default.parse(fileResponse.request.res.responseUrl);
243
- const filename = path_1.default.basename(parsedURL.pathname);
244
+ const filename = decodeURI(path_1.default.basename(parsedURL.pathname));
244
245
  const payload = {
245
246
  filename_download: filename,
246
247
  storage: (0, utils_1.toArray)(env_1.default.STORAGE_LOCATIONS)[0],
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
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
+ const exifr_1 = __importDefault(require("exifr"));
7
+ const knex_1 = __importDefault(require("knex"));
8
+ const knex_mock_client_1 = require("knex-mock-client");
9
+ const _1 = require(".");
10
+ jest.mock('exifr');
11
+ jest.mock('../../src/database/index', () => {
12
+ return { getDatabaseClient: jest.fn().mockReturnValue('postgres') };
13
+ });
14
+ jest.requireMock('../../src/database/index');
15
+ describe('Integration Tests', () => {
16
+ let db;
17
+ let tracker;
18
+ beforeAll(async () => {
19
+ db = (0, knex_1.default)({ client: knex_mock_client_1.MockClient });
20
+ tracker = (0, knex_mock_client_1.getTracker)();
21
+ });
22
+ afterEach(() => {
23
+ tracker.reset();
24
+ });
25
+ describe('Services / Files', () => {
26
+ describe('getMetadata', () => {
27
+ let service;
28
+ let exifrParseSpy;
29
+ const sampleMetadata = {
30
+ CustomTagA: 'value a',
31
+ CustomTagB: 'value b',
32
+ CustomTagC: 'value c',
33
+ };
34
+ beforeEach(() => {
35
+ exifrParseSpy = jest.spyOn(exifr_1.default, 'parse');
36
+ service = new _1.FilesService({
37
+ knex: db,
38
+ schema: { collections: {}, relations: [] },
39
+ });
40
+ });
41
+ it('accepts allowlist metadata tags', async () => {
42
+ exifrParseSpy.mockReturnValue(Promise.resolve({ ...sampleMetadata }));
43
+ const bufferContent = 'file buffer content';
44
+ const allowList = ['CustomTagB', 'CustomTagA'];
45
+ const metadata = await service.getMetadata(bufferContent, allowList);
46
+ expect(exifrParseSpy).toHaveBeenCalled();
47
+ expect(metadata.metadata.CustomTagA).toStrictEqual(sampleMetadata.CustomTagA);
48
+ expect(metadata.metadata.CustomTagB).toStrictEqual(sampleMetadata.CustomTagB);
49
+ expect(metadata.metadata.CustomTagC).toBeUndefined();
50
+ });
51
+ });
52
+ });
53
+ });
@@ -39,12 +39,16 @@ class FlowsService extends items_1.ItemsService {
39
39
  }
40
40
  async deleteOne(key, opts) {
41
41
  const flowManager = (0, flows_1.getFlowManager)();
42
+ // this is to prevent foreign key constraint error on directus_operations resolve/reject during cascade deletion
43
+ await this.knex('directus_operations').update({ resolve: null, reject: null }).where('flow', key);
42
44
  const result = await super.deleteOne(key, opts);
43
45
  await flowManager.reload();
44
46
  return result;
45
47
  }
46
48
  async deleteMany(keys, opts) {
47
49
  const flowManager = (0, flows_1.getFlowManager)();
50
+ // this is to prevent foreign key constraint error on directus_operations resolve/reject during cascade deletion
51
+ await this.knex('directus_operations').update({ resolve: null, reject: null }).whereIn('flow', keys);
48
52
  const result = await super.deleteMany(keys, opts);
49
53
  await flowManager.reload();
50
54
  return result;
@@ -1,6 +1,6 @@
1
1
  import { BaseException } from '@directus/shared/exceptions';
2
2
  import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
3
- import { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLError, GraphQLResolveInfo, GraphQLSchema, ObjectFieldNode, SelectionNode } from 'graphql';
3
+ import { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLError, GraphQLResolveInfo, GraphQLSchema, SelectionNode } from 'graphql';
4
4
  import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose';
5
5
  import { Knex } from 'knex';
6
6
  import { AbstractServiceOptions, GraphQLParams, Item } from '../../types';
@@ -44,7 +44,7 @@ export declare class GraphQLService {
44
44
  * In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
45
45
  * of arguments
46
46
  */
47
- parseArgs(args: readonly ArgumentNode[] | readonly ObjectFieldNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any>;
47
+ parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any>;
48
48
  /**
49
49
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
50
50
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
@@ -274,7 +274,7 @@ class GraphQLService {
274
274
  CollectionTypes[collection.collection] = schemaComposer.createObjectTC({
275
275
  name: action === 'read' ? collection.collection : `${action}_${collection.collection}`,
276
276
  fields: Object.values(collection.fields).reduce((acc, field) => {
277
- let type = (0, get_graphql_type_1.getGraphQLType)(field.type);
277
+ let type = (0, get_graphql_type_1.getGraphQLType)(field.type, field.special);
278
278
  // GraphQL doesn't differentiate between not-null and has-to-be-submitted. We
279
279
  // can't non-null in update, as that would require every not-nullable field to be
280
280
  // submitted on updates
@@ -639,7 +639,7 @@ class GraphQLService {
639
639
  ReadableCollectionFilterTypes[collection.collection] = schemaComposer.createInputTC({
640
640
  name: `${collection.collection}_filter`,
641
641
  fields: Object.values(collection.fields).reduce((acc, field) => {
642
- const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
642
+ const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type, field.special);
643
643
  let filterOperatorType;
644
644
  switch (graphqlType) {
645
645
  case graphql_1.GraphQLBoolean:
@@ -692,7 +692,7 @@ class GraphQLService {
692
692
  AggregatedFields[collection.collection] = schemaComposer.createObjectTC({
693
693
  name: `${collection.collection}_aggregated_fields`,
694
694
  fields: Object.values(collection.fields).reduce((acc, field) => {
695
- const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
695
+ const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type, field.special);
696
696
  switch (graphqlType) {
697
697
  case graphql_1.GraphQLInt:
698
698
  case graphql_1.GraphQLFloat:
@@ -736,7 +736,7 @@ class GraphQLService {
736
736
  },
737
737
  };
738
738
  const hasNumericAggregates = Object.values(collection.fields).some((field) => {
739
- const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type);
739
+ const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type, field.special);
740
740
  if (graphqlType === graphql_1.GraphQLInt || graphqlType === graphql_1.GraphQLFloat) {
741
741
  return true;
742
742
  }
@@ -1207,38 +1207,29 @@ class GraphQLService {
1207
1207
  parseArgs(args, variableValues) {
1208
1208
  if (!args || args.length === 0)
1209
1209
  return {};
1210
- const parseObjectValue = (arg) => {
1211
- return this.parseArgs(arg.fields, variableValues);
1212
- };
1213
- const argsObject = {};
1214
- for (const argument of args) {
1215
- if (argument.value.kind === 'ObjectValue') {
1216
- argsObject[argument.name.value] = parseObjectValue(argument.value);
1217
- }
1218
- else if (argument.value.kind === 'Variable') {
1219
- argsObject[argument.name.value] = variableValues[argument.value.name.value];
1210
+ const parse = (node) => {
1211
+ switch (node.kind) {
1212
+ case 'Variable':
1213
+ return variableValues[node.name.value];
1214
+ case 'ListValue':
1215
+ return node.values.map(parse);
1216
+ case 'ObjectValue':
1217
+ return Object.fromEntries(node.fields.map((node) => [node.name.value, parse(node.value)]));
1218
+ case 'NullValue':
1219
+ return null;
1220
+ case 'StringValue':
1221
+ return String(node.value);
1222
+ case 'IntValue':
1223
+ case 'FloatValue':
1224
+ return Number(node.value);
1225
+ case 'BooleanValue':
1226
+ return Boolean(node.value);
1227
+ case 'EnumValue':
1228
+ default:
1229
+ return node.value;
1220
1230
  }
1221
- else if (argument.value.kind === 'ListValue') {
1222
- const values = [];
1223
- for (const valueNode of argument.value.values) {
1224
- if (valueNode.kind === 'ObjectValue') {
1225
- values.push(this.parseArgs(valueNode.fields, variableValues));
1226
- }
1227
- else {
1228
- if (valueNode.kind === 'Variable') {
1229
- values.push(variableValues[valueNode.name.value]);
1230
- }
1231
- else {
1232
- values.push(valueNode.value);
1233
- }
1234
- }
1235
- }
1236
- argsObject[argument.name.value] = values;
1237
- }
1238
- else {
1239
- argsObject[argument.name.value] = argument.value.value;
1240
- }
1241
- }
1231
+ };
1232
+ const argsObject = Object.fromEntries(args.map((arg) => [arg.name.value, parse(arg.value)]));
1242
1233
  return argsObject;
1243
1234
  }
1244
1235
  /**
@@ -1902,7 +1893,9 @@ class GraphQLService {
1902
1893
  name: 'directus_collections_meta',
1903
1894
  fields: Object.values(schema.read.collections['directus_collections'].fields).reduce((acc, field) => {
1904
1895
  acc[field.field] = {
1905
- type: field.nullable ? (0, get_graphql_type_1.getGraphQLType)(field.type) : (0, graphql_1.GraphQLNonNull)((0, get_graphql_type_1.getGraphQLType)(field.type)),
1896
+ type: field.nullable
1897
+ ? (0, get_graphql_type_1.getGraphQLType)(field.type, field.special)
1898
+ : (0, graphql_1.GraphQLNonNull)((0, get_graphql_type_1.getGraphQLType)(field.type, field.special)),
1906
1899
  description: field.note,
1907
1900
  };
1908
1901
  return acc;
@@ -1951,7 +1944,9 @@ class GraphQLService {
1951
1944
  name: 'directus_fields_meta',
1952
1945
  fields: Object.values(schema.read.collections['directus_fields'].fields).reduce((acc, field) => {
1953
1946
  acc[field.field] = {
1954
- type: field.nullable ? (0, get_graphql_type_1.getGraphQLType)(field.type) : (0, graphql_1.GraphQLNonNull)((0, get_graphql_type_1.getGraphQLType)(field.type)),
1947
+ type: field.nullable
1948
+ ? (0, get_graphql_type_1.getGraphQLType)(field.type, field.special)
1949
+ : (0, graphql_1.GraphQLNonNull)((0, get_graphql_type_1.getGraphQLType)(field.type, field.special)),
1955
1950
  description: field.note,
1956
1951
  };
1957
1952
  return acc;
@@ -2038,7 +2033,7 @@ class GraphQLService {
2038
2033
  name: 'directus_relations_meta',
2039
2034
  fields: Object.values(schema.read.collections['directus_relations'].fields).reduce((acc, field) => {
2040
2035
  acc[field.field] = {
2041
- type: (0, get_graphql_type_1.getGraphQLType)(field.type),
2036
+ type: (0, get_graphql_type_1.getGraphQLType)(field.type, field.special),
2042
2037
  description: field.note,
2043
2038
  };
2044
2039
  return acc;
@@ -52,6 +52,7 @@ class ItemsService {
52
52
  .filter((field) => field.alias === true)
53
53
  .map((field) => field.field);
54
54
  const payload = (0, lodash_1.cloneDeep)(data);
55
+ const nestedActionEvents = [];
55
56
  // By wrapping the logic in a transaction, we make sure we automatically roll back all the
56
57
  // changes in the DB if any of the parts contained within throws an error. This also means
57
58
  // that any errors thrown in any nested relational changes will bubble up and cancel the whole
@@ -84,8 +85,8 @@ class ItemsService {
84
85
  const payloadWithPresets = this.accountability
85
86
  ? await authorizationService.validatePayload('create', this.collection, payloadAfterHooks)
86
87
  : payloadAfterHooks;
87
- const { payload: payloadWithM2O, revisions: revisionsM2O } = await payloadService.processM2O(payloadWithPresets);
88
- const { payload: payloadWithA2O, revisions: revisionsA2O } = await payloadService.processA2O(payloadWithM2O);
88
+ const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
89
+ const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
89
90
  const payloadWithoutAliases = (0, lodash_1.pick)(payloadWithA2O, (0, lodash_1.without)(fields, ...aliases));
90
91
  const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
91
92
  // In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
@@ -113,7 +114,10 @@ class ItemsService {
113
114
  // to read from it
114
115
  payload[primaryKeyField] = primaryKey;
115
116
  }
116
- const { revisions: revisionsO2M } = await payloadService.processO2M(payload, primaryKey);
117
+ const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payload, primaryKey, opts);
118
+ nestedActionEvents.push(...nestedActionEventsM2O);
119
+ nestedActionEvents.push(...nestedActionEventsA2O);
120
+ nestedActionEvents.push(...nestedActionEventsO2M);
117
121
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
118
122
  if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
119
123
  const activityService = new index_1.ActivityService({
@@ -154,17 +158,30 @@ class ItemsService {
154
158
  return primaryKey;
155
159
  });
156
160
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
157
- emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.create', `${this.collection}.items.create`] : `${this.eventScope}.create`, {
158
- payload,
159
- key: primaryKey,
160
- collection: this.collection,
161
- }, {
162
- // This hook is called async. If we would pass the transaction here, the hook can be
163
- // called after the transaction is done #5460
164
- database: this.knex || (0, database_1.default)(),
165
- schema: this.schema,
166
- accountability: this.accountability,
167
- });
161
+ const actionEvent = {
162
+ event: this.eventScope === 'items'
163
+ ? ['items.create', `${this.collection}.items.create`]
164
+ : `${this.eventScope}.create`,
165
+ meta: {
166
+ payload,
167
+ key: primaryKey,
168
+ collection: this.collection,
169
+ },
170
+ context: {
171
+ database: (0, database_1.default)(),
172
+ schema: this.schema,
173
+ accountability: this.accountability,
174
+ },
175
+ };
176
+ if (!(opts === null || opts === void 0 ? void 0 : opts.bypassEmitAction)) {
177
+ emitter_1.default.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
178
+ }
179
+ else {
180
+ opts.bypassEmitAction(actionEvent);
181
+ }
182
+ for (const nestedActionEvent of nestedActionEvents) {
183
+ emitter_1.default.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
184
+ }
168
185
  }
169
186
  if (this.cache && env_1.default.CACHE_AUTO_PURGE && (opts === null || opts === void 0 ? void 0 : opts.autoPurgeCache) !== false) {
170
187
  await this.cache.clear();
@@ -323,6 +340,7 @@ class ItemsService {
323
340
  .filter((field) => field.alias === true)
324
341
  .map((field) => field.field);
325
342
  const payload = (0, lodash_1.cloneDeep)(data);
343
+ const nestedActionEvents = [];
326
344
  const authorizationService = new authorization_1.AuthorizationService({
327
345
  accountability: this.accountability,
328
346
  knex: this.knex,
@@ -356,8 +374,8 @@ class ItemsService {
356
374
  knex: trx,
357
375
  schema: this.schema,
358
376
  });
359
- const { payload: payloadWithM2O, revisions: revisionsM2O } = await payloadService.processM2O(payloadWithPresets);
360
- const { payload: payloadWithA2O, revisions: revisionsA2O } = await payloadService.processA2O(payloadWithM2O);
377
+ const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
378
+ const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
361
379
  const payloadWithoutAliasAndPK = (0, lodash_1.pick)(payloadWithA2O, (0, lodash_1.without)(fields, primaryKeyField, ...aliases));
362
380
  const payloadWithTypeCasting = await payloadService.processValues('update', payloadWithoutAliasAndPK);
363
381
  if (Object.keys(payloadWithTypeCasting).length > 0) {
@@ -369,9 +387,12 @@ class ItemsService {
369
387
  }
370
388
  }
371
389
  const childrenRevisions = [...revisionsM2O, ...revisionsA2O];
390
+ nestedActionEvents.push(...nestedActionEventsM2O);
391
+ nestedActionEvents.push(...nestedActionEventsA2O);
372
392
  for (const key of keys) {
373
- const { revisions } = await payloadService.processO2M(payload, key);
393
+ const { revisions, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payload, key, opts);
374
394
  childrenRevisions.push(...revisions);
395
+ nestedActionEvents.push(...nestedActionEventsO2M);
375
396
  }
376
397
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
377
398
  if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
@@ -427,17 +448,30 @@ class ItemsService {
427
448
  await this.cache.clear();
428
449
  }
429
450
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
430
- emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.update', `${this.collection}.items.update`] : `${this.eventScope}.update`, {
431
- payload,
432
- keys,
433
- collection: this.collection,
434
- }, {
435
- // This hook is called async. If we would pass the transaction here, the hook can be
436
- // called after the transaction is done #5460
437
- database: this.knex || (0, database_1.default)(),
438
- schema: this.schema,
439
- accountability: this.accountability,
440
- });
451
+ const actionEvent = {
452
+ event: this.eventScope === 'items'
453
+ ? ['items.update', `${this.collection}.items.update`]
454
+ : `${this.eventScope}.update`,
455
+ meta: {
456
+ payload,
457
+ keys,
458
+ collection: this.collection,
459
+ },
460
+ context: {
461
+ database: (0, database_1.default)(),
462
+ schema: this.schema,
463
+ accountability: this.accountability,
464
+ },
465
+ };
466
+ if (!(opts === null || opts === void 0 ? void 0 : opts.bypassEmitAction)) {
467
+ emitter_1.default.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
468
+ }
469
+ else {
470
+ opts.bypassEmitAction(actionEvent);
471
+ }
472
+ for (const nestedActionEvent of nestedActionEvents) {
473
+ emitter_1.default.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
474
+ }
441
475
  }
442
476
  return keys;
443
477
  }
@@ -547,17 +581,27 @@ class ItemsService {
547
581
  await this.cache.clear();
548
582
  }
549
583
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
550
- emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`, {
551
- payload: keys,
552
- keys: keys,
553
- collection: this.collection,
554
- }, {
555
- // This hook is called async. If we would pass the transaction here, the hook can be
556
- // called after the transaction is done #5460
557
- database: this.knex || (0, database_1.default)(),
558
- schema: this.schema,
559
- accountability: this.accountability,
560
- });
584
+ const actionEvent = {
585
+ event: this.eventScope === 'items'
586
+ ? ['items.delete', `${this.collection}.items.delete`]
587
+ : `${this.eventScope}.delete`,
588
+ meta: {
589
+ payload: keys,
590
+ keys: keys,
591
+ collection: this.collection,
592
+ },
593
+ context: {
594
+ database: (0, database_1.default)(),
595
+ schema: this.schema,
596
+ accountability: this.accountability,
597
+ },
598
+ };
599
+ if (!(opts === null || opts === void 0 ? void 0 : opts.bypassEmitAction)) {
600
+ emitter_1.default.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
601
+ }
602
+ else {
603
+ opts.bypassEmitAction(actionEvent);
604
+ }
561
605
  }
562
606
  return keys;
563
607
  }
@@ -0,0 +1 @@
1
+ export {};