directus 9.17.4 → 9.18.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.
package/dist/app.js CHANGED
@@ -168,7 +168,7 @@ async function createApp() {
168
168
  res.send(constants_1.ROBOTSTXT);
169
169
  });
170
170
  if (env_1.default.SERVE_APP) {
171
- const adminPath = require.resolve('@directus/app', require.main ? { paths: [require.main.filename] } : undefined);
171
+ const adminPath = require.resolve('@directus/app');
172
172
  const adminUrl = new url_1.Url(env_1.default.PUBLIC_URL).addPath('admin');
173
173
  // Set the App's base path according to the APIs public URL
174
174
  const html = await fs_extra_1.default.readFile(adminPath, 'utf8');
@@ -18,7 +18,11 @@ async function usersPasswd({ email, password }) {
18
18
  const passwordHashed = await (0, generate_hash_1.generateHash)(password);
19
19
  const schema = await (0, get_schema_1.getSchema)();
20
20
  const service = new services_1.UsersService({ schema, knex: database });
21
- const user = await service.knex.select('id').from('directus_users').where({ email }).first();
21
+ const user = await service.knex
22
+ .select('id')
23
+ .from('directus_users')
24
+ .whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()])
25
+ .first();
22
26
  if (user) {
23
27
  await service.knex('directus_users').update({ password: passwordHashed }).where({ id: user.id });
24
28
  logger_1.default.info(`Password is updated for user ${user.id}`);
@@ -251,7 +251,7 @@ CORS_MAX_AGE=18000
251
251
  # The amount of threads to compute the hash on. Each thread has a memory pool with HASH_MEMORY_COST size [1]
252
252
  # HASH_PARALLELISM=2
253
253
 
254
- # The variant of the hash function (0: argon2d, 1: argon2i, or 2: argon2id) [1]
254
+ # The variant of the hash function (0: argon2d, 1: argon2i, or 2: argon2id) [2]
255
255
  # HASH_TYPE=2
256
256
 
257
257
  # An extra and optional non-secret value. The value will be included B64 encoded in the parameters portion of the digest []
@@ -169,7 +169,7 @@ class ExtensionManager {
169
169
  if (!this.watcher) {
170
170
  logger_1.default.info('Watching extensions for changes...');
171
171
  const localExtensionPaths = (env_1.default.SERVE_APP ? constants_1.EXTENSION_TYPES : constants_1.API_OR_HYBRID_EXTENSION_TYPES).flatMap((type) => {
172
- const typeDir = path_1.default.posix.join(path_1.default.relative('.', env_1.default.EXTENSIONS_PATH).split(path_1.default.sep).join(path_1.default.posix.sep), (0, utils_1.pluralize)(type));
172
+ const typeDir = path_1.default.posix.join((0, node_1.pathToRelativeUrl)(env_1.default.EXTENSIONS_PATH), (0, utils_1.pluralize)(type));
173
173
  return (0, utils_1.isIn)(type, constants_1.HYBRID_EXTENSION_TYPES)
174
174
  ? [path_1.default.posix.join(typeDir, '*', 'app.js'), path_1.default.posix.join(typeDir, '*', 'api.js')]
175
175
  : path_1.default.posix.join(typeDir, '*', 'index.js');
@@ -239,8 +239,7 @@ class ExtensionManager {
239
239
  return bundles;
240
240
  }
241
241
  async getSharedDepsMapping(deps) {
242
- var _a;
243
- const appDir = await fs_extra_1.default.readdir(path_1.default.join((0, node_1.resolvePackage)('@directus/app', (_a = require.main) === null || _a === void 0 ? void 0 : _a.filename), 'dist', 'assets'));
242
+ const appDir = await fs_extra_1.default.readdir(path_1.default.join((0, node_1.resolvePackage)('@directus/app', __dirname), 'dist', 'assets'));
244
243
  const depsMapping = {};
245
244
  for (const dep of deps) {
246
245
  const depRegex = new RegExp(`${(0, lodash_1.escapeRegExp)(dep.replace(/\//g, '_'))}\\.[0-9a-f]{8}\\.entry\\.js`);
@@ -286,7 +285,7 @@ class ExtensionManager {
286
285
  }
287
286
  }
288
287
  async registerOperations() {
289
- const internalPaths = await (0, globby_1.default)(path_1.default.posix.join(path_1.default.relative('.', __dirname).split(path_1.default.sep).join(path_1.default.posix.sep), 'operations/*/index.(js|ts)'));
288
+ const internalPaths = await (0, globby_1.default)(path_1.default.posix.join((0, node_1.pathToRelativeUrl)(__dirname), 'operations/*/index.(js|ts)'));
290
289
  const internalOperations = internalPaths.map((internalPath) => {
291
290
  const dirs = internalPath.split(path_1.default.sep);
292
291
  return {
@@ -12,7 +12,7 @@ const logger_1 = __importDefault(require("../logger"));
12
12
  const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next) => {
13
13
  var _a, _b, _c, _d;
14
14
  const { cache } = (0, cache_1.getCache)();
15
- if (req.method.toLowerCase() !== 'get' && ((_a = req.path) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql')) === false)
15
+ if (req.method.toLowerCase() !== 'get' && ((_a = req.originalUrl) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql')) === false)
16
16
  return next();
17
17
  if (env_1.default.CACHE_ENABLED !== true)
18
18
  return next();
@@ -24,7 +24,7 @@ exports.respond = (0, async_handler_1.default)(async (req, res) => {
24
24
  const maxSize = (0, bytes_1.parse)(env_1.default.CACHE_VALUE_MAX_SIZE);
25
25
  exceedsMaxSize = valueSize > maxSize;
26
26
  }
27
- if ((req.method.toLowerCase() === 'get' || ((_a = req.path) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql'))) &&
27
+ if ((req.method.toLowerCase() === 'get' || ((_a = req.originalUrl) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql'))) &&
28
28
  env_1.default.CACHE_ENABLED === true &&
29
29
  cache &&
30
30
  !req.sanitizedQuery.export &&
@@ -13,7 +13,7 @@ exports.default = (0, utils_1.defineOperationApi)({
13
13
  customAccountability = accountability;
14
14
  }
15
15
  else if (permissions === '$full') {
16
- customAccountability = null;
16
+ customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)('system', { database, schema, accountability });
17
17
  }
18
18
  else if (permissions === '$public') {
19
19
  customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)(null, { database, schema, accountability });
@@ -13,7 +13,7 @@ exports.default = (0, utils_1.defineOperationApi)({
13
13
  customAccountability = accountability;
14
14
  }
15
15
  else if (permissions === '$full') {
16
- customAccountability = null;
16
+ customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)('system', { database, schema, accountability });
17
17
  }
18
18
  else if (permissions === '$public') {
19
19
  customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)(null, { database, schema, accountability });
@@ -13,7 +13,7 @@ exports.default = (0, utils_1.defineOperationApi)({
13
13
  customAccountability = accountability;
14
14
  }
15
15
  else if (permissions === '$full') {
16
- customAccountability = null;
16
+ customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)('system', { database, schema, accountability });
17
17
  }
18
18
  else if (permissions === '$public') {
19
19
  customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)(null, { database, schema, accountability });
@@ -14,7 +14,7 @@ exports.default = (0, utils_1.defineOperationApi)({
14
14
  customAccountability = accountability;
15
15
  }
16
16
  else if (permissions === '$full') {
17
- customAccountability = null;
17
+ customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)('system', { database, schema, accountability });
18
18
  }
19
19
  else if (permissions === '$public') {
20
20
  customAccountability = await (0, get_accountability_for_role_1.getAccountabilityForRole)(null, { database, schema, accountability });
@@ -1,6 +1,7 @@
1
1
  declare type Options = {
2
2
  body: string;
3
3
  to: string;
4
+ type: 'wysiwyg' | 'markdown';
4
5
  subject: string;
5
6
  };
6
7
  declare const _default: import("@directus/shared/types").OperationApiConfig<Options>;
@@ -5,10 +5,10 @@ const services_1 = require("../../services");
5
5
  const md_1 = require("../../utils/md");
6
6
  exports.default = (0, utils_1.defineOperationApi)({
7
7
  id: 'mail',
8
- handler: async ({ body, to, subject }, { accountability, database, getSchema }) => {
8
+ handler: async ({ body, to, type, subject }, { accountability, database, getSchema }) => {
9
9
  const mailService = new services_1.MailService({ schema: await getSchema({ database }), accountability, knex: database });
10
10
  await mailService.send({
11
- html: (0, md_1.md)(body),
11
+ html: type === 'wysiwyg' ? body : (0, md_1.md)(body),
12
12
  to,
13
13
  subject,
14
14
  });
@@ -139,7 +139,9 @@ class AssetsService {
139
139
  const transformer = (0, sharp_1.default)({
140
140
  limitInputPixels: Math.pow(env_1.default.ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION, 2),
141
141
  sequentialRead: true,
142
- }).rotate();
142
+ });
143
+ if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
144
+ transformer.rotate();
143
145
  transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
144
146
  readStream.on('error', (e) => {
145
147
  logger_1.default.error(e, `Couldn't transform file ${file.id}`);
@@ -279,12 +279,18 @@ class GraphQLService {
279
279
  // can't non-null in update, as that would require every not-nullable field to be
280
280
  // submitted on updates
281
281
  if (field.nullable === false &&
282
+ !field.defaultValue &&
282
283
  !constants_1.GENERATE_SPECIAL.some((flag) => field.special.includes(flag)) &&
283
284
  action !== 'update') {
284
285
  type = (0, graphql_1.GraphQLNonNull)(type);
285
286
  }
286
287
  if (collection.primary === field.field) {
287
- type = graphql_1.GraphQLID;
288
+ if (!field.defaultValue && !field.special.includes('uuid') && action === 'create')
289
+ type = (0, graphql_1.GraphQLNonNull)(graphql_1.GraphQLID);
290
+ else if (['create', 'update'].includes(action))
291
+ type = graphql_1.GraphQLID;
292
+ else
293
+ type = (0, graphql_1.GraphQLNonNull)(graphql_1.GraphQLID);
288
294
  }
289
295
  acc[field.field] = {
290
296
  type,
@@ -397,7 +403,7 @@ class GraphQLService {
397
403
  * Create readable types and attach resolvers for each. Also prepares full filter argument structures
398
404
  */
399
405
  function getReadableTypes() {
400
- var _a, _b, _c, _d, _e, _f;
406
+ var _a, _b, _c, _d, _e, _f, _g, _h;
401
407
  const { CollectionTypes: ReadCollectionTypes } = getTypes('read');
402
408
  const ReadableCollectionFilterTypes = {};
403
409
  const AggregatedFunctions = {};
@@ -894,10 +900,12 @@ class GraphQLService {
894
900
  }
895
901
  }
896
902
  else if ((_f = relation.meta) === null || _f === void 0 ? void 0 : _f.one_allowed_collections) {
897
- /**
898
- * @TODO
899
- * Looking to add nested typed filters per union type? This is where that's supposed to go.
900
- */
903
+ (_g = ReadableCollectionFilterTypes[relation.collection]) === null || _g === void 0 ? void 0 : _g.removeField('item');
904
+ for (const collection of relation.meta.one_allowed_collections) {
905
+ (_h = ReadableCollectionFilterTypes[relation.collection]) === null || _h === void 0 ? void 0 : _h.addFields({
906
+ [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
907
+ });
908
+ }
901
909
  }
902
910
  }
903
911
  return { ReadCollectionTypes, ReadableCollectionFilterTypes };
@@ -216,7 +216,9 @@ class ItemsService {
216
216
  */
217
217
  async readByQuery(query, opts) {
218
218
  const updatedQuery = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
219
- ? await emitter_1.default.emitFilter(`${this.eventScope}.query`, query, {
219
+ ? await emitter_1.default.emitFilter(this.eventScope === 'items'
220
+ ? ['items.query', `${this.collection}.items.query`]
221
+ : `${this.eventScope}.query`, query, {
220
222
  collection: this.collection,
221
223
  }, {
222
224
  database: this.knex,
@@ -292,7 +292,11 @@ class UsersService extends items_1.ItemsService {
292
292
  }
293
293
  const STALL_TIME = 500;
294
294
  const timeStart = perf_hooks_1.performance.now();
295
- const user = await this.knex.select('status', 'password').from('directus_users').where({ email }).first();
295
+ const user = await this.knex
296
+ .select('status', 'password')
297
+ .from('directus_users')
298
+ .whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()])
299
+ .first();
296
300
  if ((user === null || user === void 0 ? void 0 : user.status) !== 'active') {
297
301
  await (0, stall_1.stall)(STALL_TIME, timeStart);
298
302
  throw new exceptions_2.ForbiddenException();
@@ -14,6 +14,15 @@ async function getAccountabilityForRole(role, context) {
14
14
  };
15
15
  generatedAccountability.permissions = await (0, get_permissions_1.getPermissions)(generatedAccountability, context.schema);
16
16
  }
17
+ else if (role === 'system') {
18
+ generatedAccountability = {
19
+ user: null,
20
+ role: null,
21
+ admin: true,
22
+ app: true,
23
+ permissions: [],
24
+ };
25
+ }
17
26
  else {
18
27
  const roleInfo = await context.database
19
28
  .select(['app_access', 'admin_access'])
@@ -8,13 +8,14 @@ const url_1 = __importDefault(require("url"));
8
8
  const object_hash_1 = __importDefault(require("object-hash"));
9
9
  const lodash_1 = require("lodash");
10
10
  function getCacheKey(req) {
11
- var _a;
11
+ var _a, _b;
12
12
  const path = url_1.default.parse(req.originalUrl).pathname;
13
13
  const isGraphQl = path === null || path === void 0 ? void 0 : path.includes('/graphql');
14
+ const isGet = ((_a = req.method) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'get';
14
15
  const info = {
15
- user: ((_a = req.accountability) === null || _a === void 0 ? void 0 : _a.user) || null,
16
+ user: ((_b = req.accountability) === null || _b === void 0 ? void 0 : _b.user) || null,
16
17
  path,
17
- query: isGraphQl ? (0, lodash_1.pick)(req.query, ['query', 'variables']) : req.sanitizedQuery,
18
+ query: isGraphQl ? (0, lodash_1.pick)(isGet ? req.query : req.body, ['query', 'variables']) : req.sanitizedQuery,
18
19
  };
19
20
  const key = (0, object_hash_1.default)(info);
20
21
  return key;
@@ -4,32 +4,48 @@ const get_cache_key_1 = require("../../src/utils/get-cache-key");
4
4
  const restUrl = 'http://localhost/items/example';
5
5
  const graphQlUrl = 'http://localhost/graphql';
6
6
  const accountability = { user: '00000000-0000-0000-0000-000000000000' };
7
+ const method = 'GET';
7
8
  const requests = [
8
9
  {
9
10
  name: 'as unauthenticated request',
10
- params: { originalUrl: restUrl },
11
+ params: { method, originalUrl: restUrl },
11
12
  key: '17da8272c9a0ec6eea38a37d6d78bddeb7c79045',
12
13
  },
13
14
  {
14
15
  name: 'as authenticated request',
15
- params: { originalUrl: restUrl, accountability },
16
+ params: { method, originalUrl: restUrl, accountability },
16
17
  key: '99a6394222a3d7d149ac1662fc2fff506932db58',
17
18
  },
18
19
  {
19
20
  name: 'a request with a fields query',
20
- params: { originalUrl: restUrl, sanitizedQuery: { fields: ['id', 'name'] } },
21
+ params: { method, originalUrl: restUrl, sanitizedQuery: { fields: ['id', 'name'] } },
21
22
  key: 'aa6e2d8a78de4dfb4af6eaa230d1cd9b7d31ed19',
22
23
  },
23
24
  {
24
25
  name: 'a request with a filter query',
25
- params: { originalUrl: restUrl, sanitizedQuery: { filter: { name: { _eq: 'test' } } } },
26
+ params: { method, originalUrl: restUrl, sanitizedQuery: { filter: { name: { _eq: 'test' } } } },
26
27
  key: 'd7eb8970f0429e1cf85e12eb5bb8669f618b09d3',
27
28
  },
28
29
  {
29
- name: 'a GraphQL query request',
30
- params: { originalUrl: graphQlUrl, query: { query: 'query { test { id } }' } },
30
+ name: 'a GraphQL GET query request',
31
+ params: { method, originalUrl: graphQlUrl, query: { query: 'query { test { id } }' } },
31
32
  key: '201731b75c627c60554512d819b6935b54c73814',
32
33
  },
34
+ {
35
+ name: 'a GraphQL POST query request',
36
+ params: { method: 'POST', originalUrl: graphQlUrl, body: { query: 'query { test { name } }' } },
37
+ key: '64eb0c48ea69d0863ff930398f29b5c7884f88f7',
38
+ },
39
+ {
40
+ name: 'an authenticated GraphQL GET query request',
41
+ params: { method, originalUrl: graphQlUrl, accountability, query: { query: 'query { test { id } }' } },
42
+ key: '9bc52c98dcf2de04c64589f52e0ada1e38d53a90',
43
+ },
44
+ {
45
+ name: 'an authenticated GraphQL POST query request',
46
+ params: { method: 'POST', originalUrl: graphQlUrl, accountability, body: { query: 'query { test { name } }' } },
47
+ key: '051ea77ce5ba71bbc88bcb567b9ddc602b585c13',
48
+ },
33
49
  ];
34
50
  const cases = requests.map(({ name, params, key }) => [name, params, key]);
35
51
  describe('get cache key', () => {
@@ -37,7 +53,7 @@ describe('get cache key', () => {
37
53
  expect((0, get_cache_key_1.getCacheKey)(params)).toEqual(key);
38
54
  });
39
55
  test('should create a unique key for each request', () => {
40
- const keys = requests.map((r) => r.key);
56
+ const keys = cases.map(([, params]) => (0, get_cache_key_1.getCacheKey)(params));
41
57
  const hasDuplicate = keys.some((key) => keys.indexOf(key) !== keys.lastIndexOf(key));
42
58
  expect(hasDuplicate).toBeFalsy();
43
59
  });
@@ -46,8 +62,13 @@ describe('get cache key', () => {
46
62
  const operationName = 'test';
47
63
  const variables1 = JSON.stringify({ name: 'test 1' });
48
64
  const variables2 = JSON.stringify({ name: 'test 2' });
49
- const req1 = { originalUrl: graphQlUrl, query: { query, operationName, variables: variables1 } };
50
- const req2 = { originalUrl: graphQlUrl, query: { query, operationName, variables: variables2 } };
65
+ const req1 = { method, originalUrl: graphQlUrl, query: { query, operationName, variables: variables1 } };
66
+ const req2 = { method, originalUrl: graphQlUrl, query: { query, operationName, variables: variables2 } };
67
+ const postReq1 = { method: 'POST', originalUrl: req1.originalUrl, body: req1.query };
68
+ const postReq2 = { method: 'POST', originalUrl: req2.originalUrl, body: req2.query };
51
69
  expect((0, get_cache_key_1.getCacheKey)(req1)).not.toEqual((0, get_cache_key_1.getCacheKey)(req2));
70
+ expect((0, get_cache_key_1.getCacheKey)(postReq1)).not.toEqual((0, get_cache_key_1.getCacheKey)(postReq2));
71
+ expect((0, get_cache_key_1.getCacheKey)(req1)).toEqual((0, get_cache_key_1.getCacheKey)(postReq1));
72
+ expect((0, get_cache_key_1.getCacheKey)(req2)).toEqual((0, get_cache_key_1.getCacheKey)(postReq2));
52
73
  });
53
74
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.17.4",
3
+ "version": "9.18.1",
4
4
  "license": "GPL-3.0-only",
5
5
  "homepage": "https://github.com/directus/directus#readme",
6
6
  "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -66,16 +66,16 @@
66
66
  ],
67
67
  "dependencies": {
68
68
  "@aws-sdk/client-ses": "^3.107.0",
69
- "@directus/app": "9.17.4",
70
- "@directus/drive": "9.17.4",
71
- "@directus/drive-azure": "9.17.4",
72
- "@directus/drive-gcs": "9.17.4",
73
- "@directus/drive-s3": "9.17.4",
69
+ "@directus/app": "9.18.1",
70
+ "@directus/drive": "9.18.1",
71
+ "@directus/drive-azure": "9.18.1",
72
+ "@directus/drive-gcs": "9.18.1",
73
+ "@directus/drive-s3": "9.18.1",
74
74
  "@directus/extensions-sdk": "^9.14.1",
75
75
  "@directus/format-title": "^9.15.0",
76
- "@directus/schema": "9.17.4",
77
- "@directus/shared": "9.17.4",
78
- "@directus/specs": "9.17.4",
76
+ "@directus/schema": "9.18.1",
77
+ "@directus/shared": "9.18.1",
78
+ "@directus/specs": "9.18.1",
79
79
  "@godaddy/terminus": "^4.10.2",
80
80
  "@rollup/plugin-alias": "^3.1.9",
81
81
  "@rollup/plugin-virtual": "^2.1.0",