directus 9.14.5 → 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 (129) 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 +2 -1
  8. package/dist/auth/drivers/ldap.js +18 -8
  9. package/dist/auth/drivers/oauth2.js +19 -9
  10. package/dist/auth/drivers/openid.js +19 -9
  11. package/dist/cache.d.ts +3 -0
  12. package/dist/cache.js +21 -2
  13. package/dist/cli/index.test.d.ts +1 -0
  14. package/dist/cli/index.test.js +58 -0
  15. package/dist/cli/utils/create-env/env-stub.liquid +2 -2
  16. package/dist/constants.d.ts +1 -0
  17. package/dist/constants.js +2 -1
  18. package/dist/controllers/assets.js +27 -1
  19. package/dist/controllers/extensions.js +3 -2
  20. package/dist/controllers/files.test.d.ts +1 -0
  21. package/dist/controllers/files.test.js +49 -0
  22. package/dist/controllers/server.js +0 -1
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -14
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -8
  25. package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -10
  26. package/dist/database/helpers/schema/dialects/oracle.js +2 -5
  27. package/dist/database/helpers/schema/types.d.ts +8 -18
  28. package/dist/database/helpers/schema/types.js +7 -36
  29. package/dist/database/migrations/20201105B-change-webhook-url-type.js +3 -2
  30. package/dist/database/migrations/20210312A-webhooks-collections-text.js +3 -2
  31. package/dist/database/migrations/20210415A-make-filesize-nullable.js +2 -2
  32. package/dist/database/migrations/20210510A-restructure-relations.js +3 -3
  33. package/dist/database/migrations/20210903A-add-auth-provider.js +2 -2
  34. package/dist/database/migrations/20210907A-webhooks-collections-not-null.js +4 -2
  35. package/dist/database/migrations/20210920A-webhooks-url-not-null.js +2 -2
  36. package/dist/database/migrations/20220303A-remove-default-project-color.js +2 -2
  37. package/dist/database/migrations/20220325B-add-default-language.js +2 -2
  38. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +2 -2
  39. package/dist/database/migrations/20220801A-update-notifications-timestamp-column.d.ts +3 -0
  40. package/dist/database/migrations/20220801A-update-notifications-timestamp-column.js +19 -0
  41. package/dist/database/migrations/20220802A-add-custom-aspect-ratios.d.ts +3 -0
  42. package/dist/database/migrations/20220802A-add-custom-aspect-ratios.js +15 -0
  43. package/dist/database/migrations/run.test.d.ts +1 -0
  44. package/dist/database/migrations/run.test.js +92 -0
  45. package/dist/database/system-data/fields/settings.yaml +33 -0
  46. package/dist/env.js +8 -0
  47. package/dist/env.test.d.ts +8 -0
  48. package/dist/env.test.js +39 -0
  49. package/dist/extensions.d.ts +2 -2
  50. package/dist/extensions.js +10 -9
  51. package/dist/flows.js +2 -1
  52. package/dist/logger.js +0 -1
  53. package/dist/middleware/authenticate.test.d.ts +1 -0
  54. package/dist/middleware/authenticate.test.js +174 -0
  55. package/dist/middleware/cache.js +3 -3
  56. package/dist/middleware/extract-token.test.d.ts +1 -0
  57. package/dist/middleware/extract-token.test.js +60 -0
  58. package/dist/middleware/respond.js +2 -2
  59. package/dist/operations/exec/index.d.ts +5 -0
  60. package/dist/operations/exec/index.js +26 -0
  61. package/dist/operations/exec/index.test.d.ts +1 -0
  62. package/dist/operations/exec/index.test.js +95 -0
  63. package/dist/operations/item-create/index.js +1 -1
  64. package/dist/operations/item-delete/index.js +2 -2
  65. package/dist/operations/item-read/index.js +2 -2
  66. package/dist/operations/item-update/index.js +3 -3
  67. package/dist/operations/notification/index.js +9 -6
  68. package/dist/operations/request/index.js +22 -3
  69. package/dist/services/assets.d.ts +1 -1
  70. package/dist/services/assets.js +7 -2
  71. package/dist/services/authorization.js +2 -1
  72. package/dist/services/files.js +3 -2
  73. package/dist/services/files.test.d.ts +1 -0
  74. package/dist/services/files.test.js +53 -0
  75. package/dist/services/flows.js +4 -0
  76. package/dist/services/graphql/index.d.ts +2 -2
  77. package/dist/services/graphql/index.js +59 -40
  78. package/dist/services/graphql/types/hash.d.ts +2 -0
  79. package/dist/services/graphql/types/hash.js +9 -0
  80. package/dist/services/items.js +83 -39
  81. package/dist/services/items.test.d.ts +1 -0
  82. package/dist/services/items.test.js +765 -0
  83. package/dist/services/payload.d.ts +7 -4
  84. package/dist/services/payload.js +35 -8
  85. package/dist/services/payload.test.d.ts +1 -0
  86. package/dist/services/payload.test.js +94 -0
  87. package/dist/services/server.js +5 -3
  88. package/dist/services/specifications.js +1 -6
  89. package/dist/services/specifications.test.d.ts +1 -0
  90. package/dist/services/specifications.test.js +96 -0
  91. package/dist/types/items.d.ts +11 -0
  92. package/dist/utils/apply-query.js +15 -0
  93. package/dist/utils/apply-snapshot.js +15 -0
  94. package/dist/utils/apply-snapshot.test.d.ts +1 -0
  95. package/dist/utils/apply-snapshot.test.js +305 -0
  96. package/dist/utils/calculate-field-depth.test.d.ts +1 -0
  97. package/dist/utils/calculate-field-depth.test.js +76 -0
  98. package/dist/utils/compress.d.ts +3 -0
  99. package/dist/utils/compress.js +17 -0
  100. package/dist/utils/filter-items.test.d.ts +1 -0
  101. package/dist/utils/filter-items.test.js +60 -0
  102. package/dist/utils/get-ast-from-query.js +1 -1
  103. package/dist/utils/get-cache-key.test.d.ts +1 -0
  104. package/dist/utils/get-cache-key.test.js +53 -0
  105. package/dist/utils/get-column-path.test.d.ts +1 -0
  106. package/dist/utils/get-column-path.test.js +136 -0
  107. package/dist/utils/get-config-from-env.test.d.ts +1 -0
  108. package/dist/utils/get-config-from-env.test.js +19 -0
  109. package/dist/utils/get-graphql-type.d.ts +1 -1
  110. package/dist/utils/get-graphql-type.js +7 -1
  111. package/dist/utils/get-os-info.d.ts +9 -0
  112. package/dist/utils/get-os-info.js +47 -0
  113. package/dist/utils/get-permissions.js +2 -2
  114. package/dist/utils/get-relation-info.test.d.ts +1 -0
  115. package/dist/utils/get-relation-info.test.js +88 -0
  116. package/dist/utils/get-relation-type.test.d.ts +1 -0
  117. package/dist/utils/get-relation-type.test.js +69 -0
  118. package/dist/utils/get-schema.js +1 -2
  119. package/dist/utils/get-string-byte-size.test.d.ts +1 -0
  120. package/dist/utils/get-string-byte-size.test.js +8 -0
  121. package/dist/utils/is-directus-jwt.test.d.ts +1 -0
  122. package/dist/utils/is-directus-jwt.test.js +26 -0
  123. package/dist/utils/jwt.test.d.ts +1 -0
  124. package/dist/utils/jwt.test.js +36 -0
  125. package/dist/utils/merge-permissions.test.d.ts +1 -0
  126. package/dist/utils/merge-permissions.test.js +80 -0
  127. package/dist/utils/validate-keys.test.d.ts +1 -0
  128. package/dist/utils/validate-keys.test.js +97 -0
  129. package/package.json +12 -11
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ // @ts-nocheck
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const database_1 = __importDefault(require("../database"));
9
+ const emitter_1 = __importDefault(require("../emitter"));
10
+ const env_1 = __importDefault(require("../env"));
11
+ const exceptions_1 = require("../exceptions");
12
+ const authenticate_1 = require("./authenticate");
13
+ require("../../src/types/express.d.ts");
14
+ jest.mock('../../src/database');
15
+ jest.mock('../../src/env', () => ({
16
+ SECRET: 'test',
17
+ }));
18
+ afterEach(() => {
19
+ jest.resetAllMocks();
20
+ });
21
+ test('Short-circuits when authenticate filter is used', async () => {
22
+ const req = {
23
+ ip: '127.0.0.1',
24
+ get: jest.fn(),
25
+ };
26
+ const res = {};
27
+ const next = jest.fn();
28
+ const customAccountability = { admin: true };
29
+ jest.spyOn(emitter_1.default, 'emitFilter').mockResolvedValue(customAccountability);
30
+ await (0, authenticate_1.handler)(req, res, next);
31
+ expect(req.accountability).toEqual(customAccountability);
32
+ expect(next).toHaveBeenCalledTimes(1);
33
+ });
34
+ test('Uses default public accountability when no token is given', async () => {
35
+ const req = {
36
+ ip: '127.0.0.1',
37
+ get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
38
+ };
39
+ const res = {};
40
+ const next = jest.fn();
41
+ jest.spyOn(emitter_1.default, 'emitFilter').mockImplementation((_, payload) => payload);
42
+ await (0, authenticate_1.handler)(req, res, next);
43
+ expect(req.accountability).toEqual({
44
+ user: null,
45
+ role: null,
46
+ admin: false,
47
+ app: false,
48
+ ip: '127.0.0.1',
49
+ userAgent: 'fake-user-agent',
50
+ });
51
+ expect(next).toHaveBeenCalledTimes(1);
52
+ });
53
+ test('Sets accountability to payload contents if valid token is passed', async () => {
54
+ const userID = '3fac3c02-607f-4438-8d6e-6b8b25109b52';
55
+ const roleID = '38269fc6-6eb6-475a-93cb-479d97f73039';
56
+ const share = 'ca0ad005-f4ad-4bfe-b428-419ee8784790';
57
+ const shareScope = {
58
+ collection: 'articles',
59
+ item: 15,
60
+ };
61
+ const appAccess = true;
62
+ const adminAccess = false;
63
+ const token = jsonwebtoken_1.default.sign({
64
+ id: userID,
65
+ role: roleID,
66
+ app_access: appAccess,
67
+ admin_access: adminAccess,
68
+ share,
69
+ share_scope: shareScope,
70
+ }, env_1.default.SECRET, { issuer: 'directus' });
71
+ const req = {
72
+ ip: '127.0.0.1',
73
+ get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
74
+ token,
75
+ };
76
+ const res = {};
77
+ const next = jest.fn();
78
+ await (0, authenticate_1.handler)(req, res, next);
79
+ expect(req.accountability).toEqual({
80
+ user: userID,
81
+ role: roleID,
82
+ app: appAccess,
83
+ admin: adminAccess,
84
+ share,
85
+ share_scope: shareScope,
86
+ ip: '127.0.0.1',
87
+ userAgent: 'fake-user-agent',
88
+ });
89
+ expect(next).toHaveBeenCalledTimes(1);
90
+ // Test with 1/0 instead or true/false
91
+ next.mockClear();
92
+ req.token = jsonwebtoken_1.default.sign({
93
+ id: userID,
94
+ role: roleID,
95
+ app_access: 1,
96
+ admin_access: 0,
97
+ share,
98
+ share_scope: shareScope,
99
+ }, env_1.default.SECRET, { issuer: 'directus' });
100
+ await (0, authenticate_1.handler)(req, res, next);
101
+ expect(req.accountability).toEqual({
102
+ user: userID,
103
+ role: roleID,
104
+ app: appAccess,
105
+ admin: adminAccess,
106
+ share,
107
+ share_scope: shareScope,
108
+ ip: '127.0.0.1',
109
+ userAgent: 'fake-user-agent',
110
+ });
111
+ expect(next).toHaveBeenCalledTimes(1);
112
+ });
113
+ test('Throws InvalidCredentialsException when static token is used, but user does not exist', async () => {
114
+ jest.mocked(database_1.default).mockReturnValue({
115
+ select: jest.fn().mockReturnThis(),
116
+ from: jest.fn().mockReturnThis(),
117
+ leftJoin: jest.fn().mockReturnThis(),
118
+ where: jest.fn().mockReturnThis(),
119
+ first: jest.fn().mockResolvedValue(undefined),
120
+ });
121
+ const req = {
122
+ ip: '127.0.0.1',
123
+ get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
124
+ token: 'static-token',
125
+ };
126
+ const res = {};
127
+ const next = jest.fn();
128
+ expect((0, authenticate_1.handler)(req, res, next)).rejects.toEqual(new exceptions_1.InvalidCredentialsException());
129
+ expect(next).toHaveBeenCalledTimes(0);
130
+ });
131
+ test('Sets accountability to user information when static token is used', async () => {
132
+ const req = {
133
+ ip: '127.0.0.1',
134
+ get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
135
+ token: 'static-token',
136
+ };
137
+ const res = {};
138
+ const next = jest.fn();
139
+ const testUser = { id: 'test-id', role: 'test-role', admin_access: true, app_access: false };
140
+ const expectedAccountability = {
141
+ user: testUser.id,
142
+ role: testUser.role,
143
+ app: testUser.app_access,
144
+ admin: testUser.admin_access,
145
+ ip: '127.0.0.1',
146
+ userAgent: 'fake-user-agent',
147
+ };
148
+ jest.mocked(database_1.default).mockReturnValue({
149
+ select: jest.fn().mockReturnThis(),
150
+ from: jest.fn().mockReturnThis(),
151
+ leftJoin: jest.fn().mockReturnThis(),
152
+ where: jest.fn().mockReturnThis(),
153
+ first: jest.fn().mockResolvedValue(testUser),
154
+ });
155
+ await (0, authenticate_1.handler)(req, res, next);
156
+ expect(req.accountability).toEqual(expectedAccountability);
157
+ expect(next).toHaveBeenCalledTimes(1);
158
+ // Test for 0 / 1 instead of false / true
159
+ next.mockClear();
160
+ testUser.admin_access = 1;
161
+ testUser.app_access = 0;
162
+ await (0, authenticate_1.handler)(req, res, next);
163
+ expect(req.accountability).toEqual(expectedAccountability);
164
+ expect(next).toHaveBeenCalledTimes(1);
165
+ // Test for "1" / "0" instead of true / false
166
+ next.mockClear();
167
+ testUser.admin_access = '0';
168
+ testUser.app_access = '1';
169
+ expectedAccountability.admin = false;
170
+ expectedAccountability.app = true;
171
+ await (0, authenticate_1.handler)(req, res, next);
172
+ expect(req.accountability).toEqual(expectedAccountability);
173
+ expect(next).toHaveBeenCalledTimes(1);
174
+ });
@@ -10,7 +10,7 @@ const get_cache_headers_1 = require("../utils/get-cache-headers");
10
10
  const get_cache_key_1 = require("../utils/get-cache-key");
11
11
  const logger_1 = __importDefault(require("../logger"));
12
12
  const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next) => {
13
- var _a, _b, _c;
13
+ var _a, _b, _c, _d;
14
14
  const { cache } = (0, cache_1.getCache)();
15
15
  if (req.method.toLowerCase() !== 'get' && ((_a = req.path) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql')) === false)
16
16
  return next();
@@ -26,7 +26,7 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
26
26
  const key = (0, get_cache_key_1.getCacheKey)(req);
27
27
  let cachedData;
28
28
  try {
29
- cachedData = await cache.get(key);
29
+ cachedData = await (0, cache_1.getCacheValue)(cache, key);
30
30
  }
31
31
  catch (err) {
32
32
  logger_1.default.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`);
@@ -37,7 +37,7 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
37
37
  if (cachedData) {
38
38
  let cacheExpiryDate;
39
39
  try {
40
- cacheExpiryDate = (await cache.get(`${key}__expires_at`));
40
+ cacheExpiryDate = (_d = (await (0, cache_1.getCacheValue)(cache, `${key}__expires_at`))) === null || _d === void 0 ? void 0 : _d.exp;
41
41
  }
42
42
  catch (err) {
43
43
  logger_1.default.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`);
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,60 @@
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 extract_token_1 = __importDefault(require("../../src/middleware/extract-token"));
7
+ require("../../src/types/express.d.ts");
8
+ let mockRequest;
9
+ let mockResponse;
10
+ const nextFunction = jest.fn();
11
+ beforeEach(() => {
12
+ mockRequest = {};
13
+ mockResponse = {};
14
+ jest.clearAllMocks();
15
+ });
16
+ test('Token from query', () => {
17
+ mockRequest = {
18
+ query: {
19
+ access_token: 'test',
20
+ },
21
+ };
22
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
23
+ expect(mockRequest.token).toBe('test');
24
+ expect(nextFunction).toBeCalledTimes(1);
25
+ });
26
+ test('Token from Authorization header (capitalized)', () => {
27
+ mockRequest = {
28
+ headers: {
29
+ authorization: 'Bearer test',
30
+ },
31
+ };
32
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
33
+ expect(mockRequest.token).toBe('test');
34
+ expect(nextFunction).toBeCalledTimes(1);
35
+ });
36
+ test('Token from Authorization header (lowercase)', () => {
37
+ mockRequest = {
38
+ headers: {
39
+ authorization: 'bearer test',
40
+ },
41
+ };
42
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
43
+ expect(mockRequest.token).toBe('test');
44
+ expect(nextFunction).toBeCalledTimes(1);
45
+ });
46
+ test('Ignore the token if authorization header is too many parts', () => {
47
+ mockRequest = {
48
+ headers: {
49
+ authorization: 'bearer test what another one',
50
+ },
51
+ };
52
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
53
+ expect(mockRequest.token).toBeNull();
54
+ expect(nextFunction).toBeCalledTimes(1);
55
+ });
56
+ test('Null if no token passed', () => {
57
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
58
+ expect(mockRequest.token).toBeNull();
59
+ expect(nextFunction).toBeCalledTimes(1);
60
+ });
@@ -32,8 +32,8 @@ exports.respond = (0, async_handler_1.default)(async (req, res) => {
32
32
  exceedsMaxSize === false) {
33
33
  const key = (0, get_cache_key_1.getCacheKey)(req);
34
34
  try {
35
- await cache.set(key, res.locals.payload, (0, ms_1.default)(env_1.default.CACHE_TTL));
36
- await cache.set(`${key}__expires_at`, Date.now() + (0, ms_1.default)(env_1.default.CACHE_TTL));
35
+ await (0, cache_1.setCacheValue)(cache, key, res.locals.payload, (0, ms_1.default)(env_1.default.CACHE_TTL));
36
+ await (0, cache_1.setCacheValue)(cache, `${key}__expires_at`, { exp: Date.now() + (0, ms_1.default)(env_1.default.CACHE_TTL) });
37
37
  }
38
38
  catch (err) {
39
39
  logger_1.default.warn(err, `[cache] Couldn't set key ${key}. ${err}`);
@@ -0,0 +1,5 @@
1
+ declare type Options = {
2
+ code: string;
3
+ };
4
+ declare const _default: import("@directus/shared/types").OperationApiConfig<Options>;
5
+ export default _default;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@directus/shared/utils");
4
+ const vm2_1 = require("vm2");
5
+ exports.default = (0, utils_1.defineOperationApi)({
6
+ id: 'exec',
7
+ handler: async ({ code }, { data, env }) => {
8
+ const allowedModules = env.FLOWS_EXEC_ALLOWED_MODULES ? (0, utils_1.toArray)(env.FLOWS_EXEC_ALLOWED_MODULES) : [];
9
+ const opts = {
10
+ eval: false,
11
+ wasm: false,
12
+ };
13
+ if (allowedModules.length > 0) {
14
+ opts.require = {
15
+ external: {
16
+ modules: allowedModules,
17
+ transitive: false,
18
+ },
19
+ };
20
+ }
21
+ const vm = new vm2_1.NodeVM(opts);
22
+ const script = new vm2_1.VMScript(code).compile();
23
+ const fn = await vm.run(script);
24
+ return await fn(data);
25
+ },
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ });
@@ -32,7 +32,7 @@ exports.default = (0, utils_1.defineOperationApi)({
32
32
  result = null;
33
33
  }
34
34
  else {
35
- result = await itemsService.createMany((0, utils_1.toArray)(payloadObject), { emitEvents });
35
+ result = await itemsService.createMany((0, utils_1.toArray)(payloadObject), { emitEvents: !!emitEvents });
36
36
  }
37
37
  return result;
38
38
  },
@@ -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.deleteOne(keys[0], { emitEvents });
38
+ result = await itemsService.deleteOne(keys[0], { emitEvents: !!emitEvents });
39
39
  }
40
40
  else {
41
- result = await itemsService.deleteMany(keys, { emitEvents });
41
+ result = await itemsService.deleteMany(keys, { emitEvents: !!emitEvents });
42
42
  }
43
43
  }
44
44
  return result;
@@ -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, { emitEvents });
38
+ result = await itemsService.readOne(keys[0], sanitizedQueryObject, { emitEvents: !!emitEvents });
39
39
  }
40
40
  else {
41
- result = await itemsService.readMany(keys, sanitizedQueryObject, { emitEvents });
41
+ result = await itemsService.readMany(keys, sanitizedQueryObject, { emitEvents: !!emitEvents });
42
42
  }
43
43
  }
44
44
  return result;
@@ -35,15 +35,15 @@ exports.default = (0, utils_1.defineOperationApi)({
35
35
  }
36
36
  let result;
37
37
  if (!key || (Array.isArray(key) && key.length === 0)) {
38
- result = await itemsService.updateByQuery(sanitizedQueryObject, payloadObject, { emitEvents });
38
+ result = await itemsService.updateByQuery(sanitizedQueryObject, payloadObject, { emitEvents: !!emitEvents });
39
39
  }
40
40
  else {
41
41
  const keys = (0, utils_1.toArray)(key);
42
42
  if (keys.length === 1) {
43
- result = await itemsService.updateOne(keys[0], payloadObject, { emitEvents });
43
+ result = await itemsService.updateOne(keys[0], payloadObject, { emitEvents: !!emitEvents });
44
44
  }
45
45
  else {
46
- result = await itemsService.updateMany(keys, payloadObject, { emitEvents });
46
+ result = await itemsService.updateMany(keys, payloadObject, { emitEvents: !!emitEvents });
47
47
  }
48
48
  }
49
49
  return result;
@@ -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
  });
@@ -1,8 +1,8 @@
1
1
  /// <reference types="node" />
2
2
  import { Range, StatResponse } from '@directus/drive';
3
+ import { Accountability } from '@directus/shared/types';
3
4
  import { Knex } from 'knex';
4
5
  import { AbstractServiceOptions, TransformationParams, TransformationPreset } from '../types';
5
- import { Accountability } from '@directus/shared/types';
6
6
  import { AuthorizationService } from './authorization';
7
7
  export declare class AssetsService {
8
8
  knex: Knex;
@@ -32,13 +32,14 @@ const mime_types_1 = require("mime-types");
32
32
  const object_hash_1 = __importDefault(require("object-hash"));
33
33
  const path_1 = __importDefault(require("path"));
34
34
  const sharp_1 = __importDefault(require("sharp"));
35
+ const uuid_validate_1 = __importDefault(require("uuid-validate"));
35
36
  const database_1 = __importDefault(require("../database"));
36
37
  const env_1 = __importDefault(require("../env"));
37
38
  const exceptions_1 = require("../exceptions");
39
+ const logger_1 = __importDefault(require("../logger"));
38
40
  const storage_1 = __importDefault(require("../storage"));
39
- const authorization_1 = require("./authorization");
40
41
  const TransformationUtils = __importStar(require("../utils/transformations"));
41
- const uuid_validate_1 = __importDefault(require("uuid-validate"));
42
+ const authorization_1 = require("./authorization");
42
43
  sharp_1.default.concurrency(1);
43
44
  // Note: don't put this in the service. The service can be initialized in multiple places, but they
44
45
  // should all share the same semaphore instance.
@@ -140,6 +141,10 @@ class AssetsService {
140
141
  sequentialRead: true,
141
142
  }).rotate();
142
143
  transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
144
+ readStream.on('error', (e) => {
145
+ logger_1.default.error(e, `Couldn't transform file ${file.id}`);
146
+ readStream.unpipe(transformer);
147
+ });
143
148
  await storage_1.default.disk(file.storage).put(assetFilename, readStream.pipe(transformer), type);
144
149
  return {
145
150
  stream: storage_1.default.disk(file.storage).getStream(assetFilename, range),
@@ -13,6 +13,7 @@ const strip_function_1 = require("../utils/strip-function");
13
13
  const items_1 = require("./items");
14
14
  const payload_1 = require("./payload");
15
15
  const get_relation_info_1 = require("../utils/get-relation-info");
16
+ const constants_1 = require("../constants");
16
17
  class AuthorizationService {
17
18
  constructor(options) {
18
19
  this.knex = options.knex || (0, database_1.default)();
@@ -379,7 +380,7 @@ class AuthorizationService {
379
380
  const requiredColumns = [];
380
381
  for (const field of Object.values(this.schema.collections[collection].fields)) {
381
382
  const specials = (_g = field === null || field === void 0 ? void 0 : field.special) !== null && _g !== void 0 ? _g : [];
382
- const hasGenerateSpecial = ['uuid', 'date-created', 'role-created', 'user-created'].some((name) => specials.includes(name));
383
+ const hasGenerateSpecial = constants_1.GENERATE_SPECIAL.some((name) => specials.includes(name));
383
384
  const nullable = field.nullable || hasGenerateSpecial || field.generated;
384
385
  if (!nullable) {
385
386
  requiredColumns.push(field);
@@ -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
+ });