directus 9.9.1 → 9.10.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ view, author, and manage your raw database content. Our performant and flexible
16
16
  schema, and includes rule-based permissions, event/web hooks, custom endpoints, numerous auth options, configurable
17
17
  storage adapters, and much more.
18
18
 
19
- Current database support includes: PostgreSQL, MySQL, SQLite, MS-SQL Server, OracleDB, MariaDB, and varients such as AWS
19
+ Current database support includes: PostgreSQL, MySQL, SQLite, MS-SQL Server, OracleDB, MariaDB, and variants such as AWS
20
20
  Aurora/Redshift or Google Cloud Platform SQL.
21
21
 
22
22
  Learn more at...
package/dist/app.js CHANGED
@@ -119,6 +119,9 @@ async function createApp() {
119
119
  connectSrc: ["'self'", 'https://*'],
120
120
  },
121
121
  }, (0, get_config_from_env_1.getConfigFromEnv)('CONTENT_SECURITY_POLICY_'))));
122
+ if (env_1.default.HSTS_ENABLED) {
123
+ app.use(helmet_1.default.hsts((0, get_config_from_env_1.getConfigFromEnv)('HSTS_', ['HSTS_ENABLED'])));
124
+ }
122
125
  await emitter_1.default.emitInit('app.before', { app });
123
126
  await emitter_1.default.emitInit('middlewares.before', { app });
124
127
  app.use(logger_1.expressLogger);
@@ -78,7 +78,7 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
78
78
  return user === null || user === void 0 ? void 0 : user.id;
79
79
  }
80
80
  async getUserID(payload) {
81
- var _a;
81
+ var _a, _b;
82
82
  if (!payload.code || !payload.codeVerifier) {
83
83
  logger_1.default.trace('[OAuth2] No code or codeVerifier in payload');
84
84
  throw new exceptions_1.InvalidCredentialsException();
@@ -97,7 +97,7 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
97
97
  const { provider, emailKey, identifierKey, allowPublicRegistration } = this.config;
98
98
  const email = userInfo[emailKey !== null && emailKey !== void 0 ? emailKey : 'email'];
99
99
  // Fallback to email if explicit identifier not found
100
- const identifier = (_a = userInfo[identifierKey]) !== null && _a !== void 0 ? _a : email;
100
+ const identifier = (_b = ((_a = userInfo[identifierKey]) !== null && _a !== void 0 ? _a : email)) === null || _b === void 0 ? void 0 : _b.toString();
101
101
  if (!identifier) {
102
102
  logger_1.default.warn(`[OAuth2] Failed to find user identifier for provider "${provider}"`);
103
103
  throw new exceptions_1.InvalidCredentialsException();
@@ -85,7 +85,7 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
85
85
  return user === null || user === void 0 ? void 0 : user.id;
86
86
  }
87
87
  async getUserID(payload) {
88
- var _a;
88
+ var _a, _b;
89
89
  if (!payload.code || !payload.codeVerifier) {
90
90
  logger_1.default.trace('[OpenID] No code or codeVerifier in payload');
91
91
  throw new exceptions_1.InvalidCredentialsException();
@@ -111,7 +111,7 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
111
111
  const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail } = this.config;
112
112
  const email = userInfo.email;
113
113
  // Fallback to email if explicit identifier not found
114
- const identifier = (_a = userInfo[identifierKey !== null && identifierKey !== void 0 ? identifierKey : 'sub']) !== null && _a !== void 0 ? _a : email;
114
+ const identifier = (_b = (_a = userInfo[identifierKey !== null && identifierKey !== void 0 ? identifierKey : 'sub']) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : email;
115
115
  if (!identifier) {
116
116
  logger_1.default.warn(`[OpenID] Failed to find user identifier for provider "${provider}"`);
117
117
  throw new exceptions_1.InvalidCredentialsException();
package/dist/env.js CHANGED
@@ -70,6 +70,7 @@ const defaults = {
70
70
  RELATIONAL_BATCH_SIZE: 25000,
71
71
  EXPORT_BATCH_SIZE: 5000,
72
72
  FILE_METADATA_ALLOW_LIST: 'ifd0.Make,ifd0.Model,exif.FNumber,exif.ExposureTime,exif.FocalLength,exif.ISO',
73
+ GRAPHQL_INTROSPECTION: true,
73
74
  };
74
75
  // Allows us to force certain environment variable into a type, instead of relying
75
76
  // on the auto-parsed type in processValues. ref #3705
@@ -84,6 +85,7 @@ const typeMap = {
84
85
  DB_EXCLUDE_TABLES: 'array',
85
86
  IMPORT_IP_DENY_LIST: 'array',
86
87
  FILE_METADATA_ALLOW_LIST: 'array',
88
+ GRAPHQL_INTROSPECTION: 'boolean',
87
89
  };
88
90
  let env = {
89
91
  ...defaults,
@@ -209,6 +211,8 @@ function processValues(env) {
209
211
  case 'json':
210
212
  env[key] = tryJSON(value);
211
213
  break;
214
+ case 'boolean':
215
+ env[key] = value === 'true' || value === true || value === '1' || value === 1;
212
216
  }
213
217
  continue;
214
218
  }
@@ -89,7 +89,16 @@ class GraphQLService {
89
89
  async execute({ document, variables, operationName, contextValue, }) {
90
90
  var _a;
91
91
  const schema = this.getSchema();
92
- const validationErrors = (0, graphql_1.validate)(schema, document, graphql_1.specifiedRules);
92
+ const validationErrors = (0, graphql_1.validate)(schema, document, [
93
+ ...graphql_1.specifiedRules,
94
+ (context) => ({
95
+ Field(node) {
96
+ if (env_1.default.GRAPHQL_INTROSPECTION === false && (node.name.value === '__schema' || node.name.value === '__type')) {
97
+ context.reportError(new graphql_1.GraphQLError('GraphQL introspection is not allowed. The query contained __schema or __type.', [node]));
98
+ }
99
+ },
100
+ }),
101
+ ]);
93
102
  if (validationErrors.length > 0) {
94
103
  throw new exceptions_1.GraphQLValidationException({ graphqlErrors: validationErrors });
95
104
  }
@@ -1287,8 +1296,10 @@ class GraphQLService {
1287
1296
  */
1288
1297
  formatError(error) {
1289
1298
  if (Array.isArray(error)) {
1299
+ error[0].extensions.code = error[0].code;
1290
1300
  return new graphql_1.GraphQLError(error[0].message, undefined, undefined, undefined, undefined, error[0]);
1291
1301
  }
1302
+ error.extensions.code = error.code;
1292
1303
  return new graphql_1.GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
1293
1304
  }
1294
1305
  /**
@@ -22,6 +22,7 @@ const get_date_formatted_1 = require("../utils/get-date-formatted");
22
22
  const utils_1 = require("@directus/shared/utils");
23
23
  const notifications_1 = require("./notifications");
24
24
  const logger_1 = __importDefault(require("../logger"));
25
+ const strip_bom_stream_1 = __importDefault(require("strip-bom-stream"));
25
26
  class ImportService {
26
27
  constructor(options) {
27
28
  this.knex = options.knex || (0, database_1.default)();
@@ -91,6 +92,7 @@ class ImportService {
91
92
  });
92
93
  return new Promise((resolve, reject) => {
93
94
  stream
95
+ .pipe((0, strip_bom_stream_1.default)())
94
96
  .pipe((0, csv_parser_1.default)())
95
97
  .on('data', (value) => {
96
98
  const obj = (0, lodash_1.transform)(value, (result, value, key) => {
@@ -90,8 +90,13 @@ class ItemsService {
90
90
  // In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
91
91
  let primaryKey = payloadWithTypeCasting[primaryKeyField];
92
92
  try {
93
- const result = await trx.insert(payloadWithoutAliases).into(this.collection).returning(primaryKeyField);
94
- primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : result[0];
93
+ const result = await trx
94
+ .insert(payloadWithoutAliases)
95
+ .into(this.collection)
96
+ .returning(primaryKeyField)
97
+ .then((result) => result[0]);
98
+ const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
99
+ primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : returnedKey;
95
100
  }
96
101
  catch (err) {
97
102
  throw await (0, translate_1.translateDatabaseError)(err);
@@ -318,7 +318,7 @@ class PayloadService {
318
318
  }
319
319
  const allowedCollections = relation.meta.one_allowed_collections;
320
320
  if (allowedCollections.includes(relatedCollection) === false) {
321
- throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}`);
321
+ throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}"`);
322
322
  }
323
323
  const itemsService = new items_1.ItemsService(relatedCollection, {
324
324
  accountability: this.accountability,
@@ -1,9 +1,10 @@
1
- import { Knex } from 'knex';
2
1
  import { Aggregate, Filter, Query, SchemaOverview } from '@directus/shared/types';
2
+ import { Knex } from 'knex';
3
3
  /**
4
4
  * Apply the Query to a given Knex query builder instance
5
5
  */
6
6
  export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, subQuery?: boolean): Knex.QueryBuilder;
7
+ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootSort: string[], collection: string, subQuery?: boolean): void;
7
8
  export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, subQuery?: boolean): Knex.QueryBuilder<any, any>;
8
9
  export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
9
10
  export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
@@ -3,33 +3,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.applyAggregate = exports.applySearch = exports.applyFilter = void 0;
6
+ exports.applyAggregate = exports.applySearch = exports.applyFilter = exports.applySort = void 0;
7
+ const utils_1 = require("@directus/shared/utils");
7
8
  const lodash_1 = require("lodash");
8
9
  const nanoid_1 = require("nanoid");
9
10
  const uuid_validate_1 = __importDefault(require("uuid-validate"));
11
+ const helpers_1 = require("../database/helpers");
10
12
  const exceptions_1 = require("../exceptions");
11
13
  const get_column_1 = require("./get-column");
12
- const get_relation_type_1 = require("./get-relation-type");
13
- const helpers_1 = require("../database/helpers");
14
- const utils_1 = require("@directus/shared/utils");
14
+ const get_column_path_1 = require("./get-column-path");
15
+ const get_relation_info_1 = require("./get-relation-info");
15
16
  const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz', 5);
16
17
  /**
17
18
  * Apply the Query to a given Knex query builder instance
18
19
  */
19
20
  function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false) {
20
21
  if (query.sort) {
21
- dbQuery.orderBy(query.sort.map((sortField) => {
22
- let column = sortField;
23
- let order = 'asc';
24
- if (sortField.startsWith('-')) {
25
- column = column.substring(1);
26
- order = 'desc';
27
- }
28
- return {
29
- order,
30
- column: (0, get_column_1.getColumn)(knex, collection, column, false, schema),
31
- };
32
- }));
22
+ applySort(knex, schema, dbQuery, query.sort, collection, subQuery);
33
23
  }
34
24
  if (typeof query.limit === 'number' && query.limit !== -1) {
35
25
  dbQuery.limit(query.limit);
@@ -55,44 +45,109 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
55
45
  return dbQuery;
56
46
  }
57
47
  exports.default = applyQuery;
58
- function getRelationInfo(relations, collection, field) {
59
- var _a, _b;
60
- const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
61
- if (implicitRelation) {
62
- if (implicitRelation[2] === undefined) {
63
- const [m2oCollection, m2oField] = implicitRelation;
64
- const relation = {
65
- collection: m2oCollection,
66
- field: m2oField,
67
- related_collection: collection,
68
- schema: null,
69
- meta: null,
70
- };
71
- return { relation, relationType: 'o2m' };
48
+ function addJoin({ path, collection, aliasMap, rootQuery, subQuery, schema, relations, knex }) {
49
+ path = (0, lodash_1.clone)(path);
50
+ followRelation(path);
51
+ function followRelation(pathParts, parentCollection = collection, parentAlias) {
52
+ /**
53
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
54
+ */
55
+ const pathRoot = pathParts[0].split(':')[0];
56
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, parentCollection, pathRoot);
57
+ if (!relation) {
58
+ return;
72
59
  }
73
- else {
74
- const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
75
- const relation = {
76
- collection: a2oCollection,
77
- field: a2oItemField,
78
- related_collection: collection,
79
- schema: null,
80
- meta: {
81
- one_collection_field: a2oCollectionField,
82
- one_field: field,
83
- },
84
- };
85
- return { relation, relationType: 'o2a' };
60
+ const alias = generateAlias();
61
+ (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
62
+ if (relationType === 'm2o') {
63
+ rootQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
64
+ }
65
+ if (relationType === 'a2o') {
66
+ const pathScope = pathParts[0].split(':')[1];
67
+ if (!pathScope) {
68
+ throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
69
+ }
70
+ rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
71
+ joinClause
72
+ .onVal(relation.meta.one_collection_field, '=', pathScope)
73
+ .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
74
+ });
75
+ }
76
+ if (relationType === 'o2a') {
77
+ rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
78
+ joinClause
79
+ .onVal(relation.meta.one_collection_field, '=', parentCollection)
80
+ .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
81
+ });
82
+ }
83
+ // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
84
+ if (relationType === 'o2m' && (subQuery === true || parentAlias !== undefined)) {
85
+ rootQuery.leftJoin({ [alias]: relation.collection }, `${parentAlias || parentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
86
+ }
87
+ if (relationType === 'm2o' || subQuery === true || (relationType === 'o2m' && parentAlias !== undefined)) {
88
+ let parent;
89
+ if (relationType === 'm2o') {
90
+ parent = relation.related_collection;
91
+ }
92
+ else if (relationType === 'a2o') {
93
+ const pathScope = pathParts[0].split(':')[1];
94
+ if (!pathScope) {
95
+ throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
96
+ }
97
+ parent = pathScope;
98
+ }
99
+ else {
100
+ parent = relation.collection;
101
+ }
102
+ pathParts.shift();
103
+ if (pathParts.length) {
104
+ followRelation(pathParts, parent, alias);
105
+ }
86
106
  }
87
107
  }
88
- const relation = (_b = relations.find((relation) => {
89
- var _a;
90
- return ((relation.collection === collection && relation.field === field) ||
91
- (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
92
- })) !== null && _b !== void 0 ? _b : null;
93
- const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
94
- return { relation, relationType };
95
108
  }
109
+ function applySort(knex, schema, rootQuery, rootSort, collection, subQuery = false) {
110
+ const relations = schema.relations;
111
+ const aliasMap = {};
112
+ rootQuery.orderBy(rootSort.map((sortField) => {
113
+ const column = sortField.split('.');
114
+ let order = 'asc';
115
+ if (column.length > 1) {
116
+ if (sortField.startsWith('-')) {
117
+ order = 'desc';
118
+ }
119
+ if (column[0].startsWith('-')) {
120
+ column[0] = column[0].substring(1);
121
+ }
122
+ addJoin({
123
+ path: column,
124
+ collection,
125
+ aliasMap,
126
+ rootQuery,
127
+ subQuery,
128
+ schema,
129
+ relations,
130
+ knex,
131
+ });
132
+ const colPath = (0, get_column_path_1.getColumnPath)({ path: column, collection, aliasMap, relations }) || '';
133
+ const [alias, field] = colPath.split('.');
134
+ return {
135
+ order,
136
+ column: (0, get_column_1.getColumn)(knex, alias, field, false, schema),
137
+ };
138
+ }
139
+ let col = column[0];
140
+ if (sortField.startsWith('-')) {
141
+ col = column[0].substring(1);
142
+ order = 'desc';
143
+ }
144
+ return {
145
+ order,
146
+ column: (0, get_column_1.getColumn)(knex, collection, col, false, schema),
147
+ };
148
+ }));
149
+ }
150
+ exports.applySort = applySort;
96
151
  function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
97
152
  const helpers = (0, helpers_1.getHelpers)(knex);
98
153
  const relations = schema.relations;
@@ -114,68 +169,16 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
114
169
  }
115
170
  const filterPath = getFilterPath(key, value);
116
171
  if (filterPath.length > 1) {
117
- addJoin(filterPath, collection);
118
- }
119
- }
120
- function addJoin(path, collection) {
121
- path = (0, lodash_1.clone)(path);
122
- followRelation(path);
123
- function followRelation(pathParts, parentCollection = collection, parentAlias) {
124
- /**
125
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
126
- */
127
- const pathRoot = pathParts[0].split(':')[0];
128
- const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
129
- if (!relation) {
130
- return;
131
- }
132
- const alias = generateAlias();
133
- (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
134
- if (relationType === 'm2o') {
135
- dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
136
- }
137
- if (relationType === 'a2o') {
138
- const pathScope = pathParts[0].split(':')[1];
139
- if (!pathScope) {
140
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
141
- }
142
- dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
143
- joinClause
144
- .onVal(relation.meta.one_collection_field, '=', pathScope)
145
- .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
146
- });
147
- }
148
- if (relationType === 'o2a') {
149
- dbQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
150
- joinClause
151
- .onVal(relation.meta.one_collection_field, '=', parentCollection)
152
- .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
153
- });
154
- }
155
- // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
156
- if (relationType === 'o2m' && (subQuery === true || parentAlias !== undefined)) {
157
- dbQuery.leftJoin({ [alias]: relation.collection }, `${parentAlias || parentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
158
- }
159
- if (relationType === 'm2o' || subQuery === true || (relationType === 'o2m' && parentAlias !== undefined)) {
160
- let parent;
161
- if (relationType === 'm2o') {
162
- parent = relation.related_collection;
163
- }
164
- else if (relationType === 'a2o') {
165
- const pathScope = pathParts[0].split(':')[1];
166
- if (!pathScope) {
167
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
168
- }
169
- parent = pathScope;
170
- }
171
- else {
172
- parent = relation.collection;
173
- }
174
- pathParts.shift();
175
- if (pathParts.length) {
176
- followRelation(pathParts, parent, alias);
177
- }
178
- }
172
+ addJoin({
173
+ path: filterPath,
174
+ collection,
175
+ knex,
176
+ schema,
177
+ relations,
178
+ subQuery,
179
+ rootQuery,
180
+ aliasMap,
181
+ });
179
182
  }
180
183
  }
181
184
  }
@@ -201,11 +204,11 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
201
204
  * For A2M fields, the path can contain an optional collection scope <field>:<scope>
202
205
  */
203
206
  const pathRoot = filterPath[0].split(':')[0];
204
- const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
207
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, collection, pathRoot);
205
208
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
206
209
  if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
207
210
  if (filterPath.length > 1) {
208
- const columnName = getWhereColumn(filterPath, collection);
211
+ const columnName = (0, get_column_path_1.getColumnPath)({ path: filterPath, collection, relations, aliasMap });
209
212
  if (!columnName)
210
213
  continue;
211
214
  applyFilterToQuery(columnName, filterOperator, filterValue, logical);
@@ -382,41 +385,6 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
382
385
  dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
383
386
  }
384
387
  }
385
- function getWhereColumn(path, collection) {
386
- return followRelation(path);
387
- function followRelation(pathParts, parentCollection = collection, parentAlias) {
388
- /**
389
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
390
- */
391
- const pathRoot = pathParts[0].split(':')[0];
392
- const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
393
- if (!relation) {
394
- throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
395
- }
396
- const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
397
- const remainingParts = pathParts.slice(1);
398
- let parent;
399
- if (relationType === 'a2o') {
400
- const pathScope = pathParts[0].split(':')[1];
401
- if (!pathScope) {
402
- throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
403
- }
404
- parent = pathScope;
405
- }
406
- else if (relationType === 'm2o') {
407
- parent = relation.related_collection;
408
- }
409
- else {
410
- parent = relation.collection;
411
- }
412
- if (remainingParts.length === 1) {
413
- return `${alias || parent}.${remainingParts[0]}`;
414
- }
415
- if (remainingParts.length) {
416
- return followRelation(remainingParts, parent, alias);
417
- }
418
- }
419
- }
420
388
  }
421
389
  }
422
390
  exports.applyFilter = applyFilter;
@@ -0,0 +1,16 @@
1
+ import { Relation } from '@directus/shared/types';
2
+ declare type AliasMap = string | {
3
+ [key: string]: AliasMap;
4
+ };
5
+ export declare type ColPathProps = {
6
+ path: string[];
7
+ collection: string;
8
+ aliasMap: AliasMap;
9
+ relations: Relation[];
10
+ };
11
+ /**
12
+ * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
13
+ * For example: ['author', 'role', 'name'] -> 'ljnsv.name'
14
+ */
15
+ export declare function getColumnPath({ path, collection, aliasMap, relations }: ColPathProps): string | void;
16
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getColumnPath = void 0;
4
+ const get_relation_info_1 = require("./get-relation-info");
5
+ const exceptions_1 = require("../exceptions");
6
+ const lodash_1 = require("lodash");
7
+ /**
8
+ * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
9
+ * For example: ['author', 'role', 'name'] -> 'ljnsv.name'
10
+ */
11
+ function getColumnPath({ path, collection, aliasMap, relations }) {
12
+ return followRelation(path);
13
+ function followRelation(pathParts, parentCollection = collection, parentAlias) {
14
+ /**
15
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
16
+ */
17
+ const pathRoot = pathParts[0].split(':')[0];
18
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, parentCollection, pathRoot);
19
+ if (!relation) {
20
+ throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
21
+ }
22
+ const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
23
+ const remainingParts = pathParts.slice(1);
24
+ let parent;
25
+ if (relationType === 'a2o') {
26
+ const pathScope = pathParts[0].split(':')[1];
27
+ if (!pathScope) {
28
+ throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when sorting on a many-to-any item`);
29
+ }
30
+ parent = pathScope;
31
+ }
32
+ else if (relationType === 'm2o') {
33
+ parent = relation.related_collection;
34
+ }
35
+ else {
36
+ parent = relation.collection;
37
+ }
38
+ if (remainingParts.length === 1) {
39
+ return `${alias || parent}.${remainingParts[0]}`;
40
+ }
41
+ if (remainingParts.length) {
42
+ return followRelation(remainingParts, parent, alias);
43
+ }
44
+ }
45
+ }
46
+ exports.getColumnPath = getColumnPath;
@@ -0,0 +1,7 @@
1
+ import { Relation } from '@directus/shared/types';
2
+ declare type RelationInfo = {
3
+ relation: Relation | null;
4
+ relationType: string | null;
5
+ };
6
+ export declare function getRelationInfo(relations: Relation[], collection: string, field: string): RelationInfo;
7
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRelationInfo = void 0;
4
+ const get_relation_type_1 = require("./get-relation-type");
5
+ function getRelationInfo(relations, collection, field) {
6
+ var _a, _b;
7
+ if (field.startsWith('$FOLLOW') && field.length > 500) {
8
+ throw new Error(`Implicit $FOLLOW statement is too big to parse. Got: "${field.substring(500)}..."`);
9
+ }
10
+ const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
11
+ if (implicitRelation) {
12
+ if (implicitRelation[2] === undefined) {
13
+ const [m2oCollection, m2oField] = implicitRelation;
14
+ const relation = {
15
+ collection: m2oCollection.trim(),
16
+ field: m2oField.trim(),
17
+ related_collection: collection,
18
+ schema: null,
19
+ meta: null,
20
+ };
21
+ return { relation, relationType: 'o2m' };
22
+ }
23
+ else {
24
+ const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
25
+ const relation = {
26
+ collection: a2oCollection.trim(),
27
+ field: a2oItemField.trim(),
28
+ related_collection: collection,
29
+ schema: null,
30
+ meta: {
31
+ one_collection_field: a2oCollectionField.trim(),
32
+ },
33
+ };
34
+ return { relation, relationType: 'o2a' };
35
+ }
36
+ }
37
+ const relation = (_b = relations.find((relation) => {
38
+ var _a;
39
+ return ((relation.collection === collection && relation.field === field) ||
40
+ (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
41
+ })) !== null && _b !== void 0 ? _b : null;
42
+ const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
43
+ return { relation, relationType };
44
+ }
45
+ exports.getRelationInfo = getRelationInfo;
@@ -1,6 +1,6 @@
1
1
  import { Relation } from '@directus/shared/types';
2
2
  export declare function getRelationType(getRelationOptions: {
3
- relation: Relation;
3
+ relation?: Relation | null;
4
4
  collection: string | null;
5
5
  field: string;
6
6
  }): 'm2o' | 'o2m' | 'a2o' | null;
@@ -48,7 +48,7 @@ function mergePermissionsForShare(currentPermissions, accountability, schema) {
48
48
  }
49
49
  }
50
50
  // Explicitly filter out permissions to collections unrelated to the root parent item.
51
- const limitedPermissions = currentPermissions.filter(({ collection }) => allowedCollections.includes(collection));
51
+ const limitedPermissions = currentPermissions.filter(({ action, collection }) => allowedCollections.includes(collection) && action === 'read');
52
52
  return (0, merge_permissions_1.mergePermissions)('and', limitedPermissions, generatedPermissions);
53
53
  }
54
54
  exports.mergePermissionsForShare = mergePermissionsForShare;
@@ -10,7 +10,7 @@ const lodash_1 = require("lodash");
10
10
  * @returns Reduced schema
11
11
  */
12
12
  function reduceSchema(schema, permissions, actions = ['create', 'read', 'update', 'delete']) {
13
- var _a, _b, _c, _d, _e;
13
+ var _a, _b, _c;
14
14
  const reduced = {
15
15
  collections: {},
16
16
  relations: [],
@@ -34,10 +34,9 @@ function reduceSchema(schema, permissions, actions = ['create', 'read', 'update'
34
34
  !((_c = allowedFieldsInCollection[collectionName]) === null || _c === void 0 ? void 0 : _c.includes(fieldName))) {
35
35
  continue;
36
36
  }
37
- const relatedCollection = ((_d = schema.relations.find((relation) => relation.collection === collectionName && relation.field === fieldName)) === null || _d === void 0 ? void 0 : _d.related_collection) ||
38
- ((_e = schema.relations.find((relation) => { var _a; return relation.related_collection === collectionName && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === fieldName; })) === null || _e === void 0 ? void 0 : _e.collection);
39
- if (relatedCollection &&
40
- !(permissions === null || permissions === void 0 ? void 0 : permissions.some((permission) => permission.collection === relatedCollection && actions.includes(permission.action)))) {
37
+ const o2mRelation = schema.relations.find((relation) => { var _a; return relation.related_collection === collectionName && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === fieldName; });
38
+ if (o2mRelation &&
39
+ !(permissions === null || permissions === void 0 ? void 0 : permissions.some((permission) => permission.collection === o2mRelation.collection && actions.includes(permission.action)))) {
41
40
  continue;
42
41
  }
43
42
  fields[fieldName] = field;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.9.1",
3
+ "version": "9.10.0",
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.",
@@ -78,16 +78,16 @@
78
78
  ],
79
79
  "dependencies": {
80
80
  "@aws-sdk/client-ses": "^3.40.0",
81
- "@directus/app": "9.9.1",
82
- "@directus/drive": "9.9.1",
83
- "@directus/drive-azure": "9.9.1",
84
- "@directus/drive-gcs": "9.9.1",
85
- "@directus/drive-s3": "9.9.1",
86
- "@directus/extensions-sdk": "9.9.1",
87
- "@directus/format-title": "9.9.1",
88
- "@directus/schema": "9.9.1",
89
- "@directus/shared": "9.9.1",
90
- "@directus/specs": "9.9.1",
81
+ "@directus/app": "9.10.0",
82
+ "@directus/drive": "9.10.0",
83
+ "@directus/drive-azure": "9.10.0",
84
+ "@directus/drive-gcs": "9.10.0",
85
+ "@directus/drive-s3": "9.10.0",
86
+ "@directus/extensions-sdk": "9.10.0",
87
+ "@directus/format-title": "9.10.0",
88
+ "@directus/schema": "9.10.0",
89
+ "@directus/shared": "9.10.0",
90
+ "@directus/specs": "9.10.0",
91
91
  "@godaddy/terminus": "^4.9.0",
92
92
  "@rollup/plugin-alias": "^3.1.9",
93
93
  "@rollup/plugin-virtual": "^2.0.3",
@@ -124,7 +124,7 @@
124
124
  "json2csv": "^5.0.3",
125
125
  "jsonwebtoken": "^8.5.1",
126
126
  "keyv": "^4.0.3",
127
- "knex": "^0.95.14",
127
+ "knex": "^2.0.0",
128
128
  "knex-schema-inspector": "1.7.3",
129
129
  "ldapjs": "^2.3.1",
130
130
  "liquidjs": "^9.25.0",
@@ -152,6 +152,7 @@
152
152
  "sanitize-html": "^2.6.0",
153
153
  "sharp": "^0.30.3",
154
154
  "stream-json": "^1.7.1",
155
+ "strip-bom-stream": "^4.0.0",
155
156
  "supertest": "^6.1.6",
156
157
  "tmp-promise": "^3.0.3",
157
158
  "update-check": "^1.5.4",
@@ -161,19 +162,16 @@
161
162
  },
162
163
  "optionalDependencies": {
163
164
  "@keyv/redis": "^2.1.2",
164
- "connect-memcached": "^1.0.0",
165
- "connect-redis": "^6.0.0",
166
- "connect-session-knex": "^2.1.0",
167
165
  "ioredis": "^4.27.6",
168
166
  "keyv-memcache": "^1.2.5",
169
167
  "memcached": "^2.2.2",
170
168
  "mysql": "^2.18.1",
171
169
  "nodemailer-mailgun-transport": "^2.1.3",
172
170
  "pg": "^8.6.0",
173
- "sqlite3": "^5.0.2",
171
+ "sqlite3": "^5.0.6",
174
172
  "tedious": "^13.0.0"
175
173
  },
176
- "gitHead": "ed780aceba707c714e0b0aa01a953d141a5c800e",
174
+ "gitHead": "e3a7a7d8879fb7959fb15802734d830001108fbb",
177
175
  "devDependencies": {
178
176
  "@types/async": "3.2.10",
179
177
  "@types/body-parser": "1.19.2",
@@ -216,7 +214,7 @@
216
214
  "cross-env": "7.0.3",
217
215
  "form-data": "^4.0.0",
218
216
  "jest": "27.5.1",
219
- "knex-mock-client": "1.6.1",
217
+ "knex-mock-client": "1.7.0",
220
218
  "ts-jest": "27.1.3",
221
219
  "ts-node-dev": "1.1.8",
222
220
  "typescript": "4.5.2"