directus 9.2.0 → 9.4.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 (90) hide show
  1. package/dist/app.js +5 -3
  2. package/dist/auth/auth.d.ts +4 -6
  3. package/dist/auth/auth.js +5 -9
  4. package/dist/auth/drivers/ldap.d.ts +3 -3
  5. package/dist/auth/drivers/ldap.js +11 -4
  6. package/dist/auth/drivers/local.d.ts +2 -2
  7. package/dist/auth/drivers/local.js +5 -12
  8. package/dist/auth/drivers/oauth2.d.ts +4 -4
  9. package/dist/auth/drivers/oauth2.js +47 -21
  10. package/dist/auth/drivers/openid.d.ts +4 -4
  11. package/dist/auth/drivers/openid.js +35 -19
  12. package/dist/cli/commands/bootstrap/index.js +3 -2
  13. package/dist/cli/commands/init/index.js +3 -7
  14. package/dist/cli/commands/schema/apply.js +1 -1
  15. package/dist/cli/utils/defaults.d.ts +11 -0
  16. package/dist/cli/utils/defaults.js +14 -0
  17. package/dist/constants.d.ts +8 -0
  18. package/dist/constants.js +16 -2
  19. package/dist/controllers/shares.d.ts +2 -0
  20. package/dist/controllers/shares.js +212 -0
  21. package/dist/controllers/users.js +21 -9
  22. package/dist/database/migrations/20211211A-add-shares.d.ts +3 -0
  23. package/dist/database/migrations/20211211A-add-shares.js +37 -0
  24. package/dist/database/run-ast.js +5 -5
  25. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -15
  26. package/dist/database/system-data/app-access-permissions/index.d.ts +1 -0
  27. package/dist/database/system-data/app-access-permissions/index.js +4 -2
  28. package/dist/database/system-data/app-access-permissions/schema-access-permissions.yaml +17 -0
  29. package/dist/database/system-data/collections/collections.yaml +3 -0
  30. package/dist/database/system-data/fields/sessions.yaml +1 -1
  31. package/dist/database/system-data/fields/shares.yaml +73 -0
  32. package/dist/database/system-data/fields/users.yaml +1 -1
  33. package/dist/database/system-data/relations/relations.yaml +15 -0
  34. package/dist/emitter.d.ts +3 -2
  35. package/dist/emitter.js +13 -6
  36. package/dist/exceptions/index.d.ts +2 -0
  37. package/dist/exceptions/index.js +2 -0
  38. package/dist/exceptions/invalid-token.d.ts +4 -0
  39. package/dist/exceptions/invalid-token.js +10 -0
  40. package/dist/exceptions/unexpected-response.d.ts +4 -0
  41. package/dist/exceptions/unexpected-response.js +10 -0
  42. package/dist/extensions.d.ts +1 -0
  43. package/dist/extensions.js +10 -4
  44. package/dist/middleware/authenticate.js +5 -15
  45. package/dist/middleware/check-ip.js +9 -6
  46. package/dist/middleware/respond.js +4 -1
  47. package/dist/services/activity.d.ts +2 -1
  48. package/dist/services/activity.js +2 -2
  49. package/dist/services/authentication.d.ts +2 -7
  50. package/dist/services/authentication.js +81 -41
  51. package/dist/services/authorization.js +3 -3
  52. package/dist/services/collections.d.ts +1 -2
  53. package/dist/services/collections.js +2 -2
  54. package/dist/services/files.d.ts +2 -2
  55. package/dist/services/files.js +14 -8
  56. package/dist/services/graphql.js +16 -5
  57. package/dist/services/index.d.ts +1 -0
  58. package/dist/services/index.js +1 -0
  59. package/dist/services/items.d.ts +1 -15
  60. package/dist/services/notifications.d.ts +2 -2
  61. package/dist/services/permissions.d.ts +2 -2
  62. package/dist/services/roles.d.ts +2 -2
  63. package/dist/services/shares.d.ts +17 -0
  64. package/dist/services/shares.js +135 -0
  65. package/dist/services/specifications.js +1 -1
  66. package/dist/services/users.d.ts +2 -2
  67. package/dist/services/users.js +8 -6
  68. package/dist/services/webhooks.d.ts +2 -2
  69. package/dist/tests/database/migrations/run.test.d.ts +1 -0
  70. package/dist/tests/database/migrations/run.test.js +29 -0
  71. package/dist/types/ast.d.ts +3 -3
  72. package/dist/types/auth.d.ts +31 -0
  73. package/dist/types/extensions.d.ts +2 -0
  74. package/dist/types/items.d.ts +14 -0
  75. package/dist/utils/apply-query.d.ts +0 -38
  76. package/dist/utils/apply-query.js +67 -69
  77. package/dist/utils/apply-snapshot.js +69 -14
  78. package/dist/utils/get-ast-from-query.js +3 -3
  79. package/dist/utils/get-permissions.d.ts +2 -2
  80. package/dist/utils/get-permissions.js +117 -72
  81. package/dist/utils/get-relation-type.d.ts +1 -1
  82. package/dist/utils/get-relation-type.js +1 -1
  83. package/dist/utils/merge-permissions-for-share.d.ts +5 -0
  84. package/dist/utils/merge-permissions-for-share.js +116 -0
  85. package/dist/utils/merge-permissions.d.ts +13 -1
  86. package/dist/utils/merge-permissions.js +29 -21
  87. package/dist/utils/reduce-schema.d.ts +2 -2
  88. package/dist/utils/reduce-schema.js +7 -7
  89. package/dist/utils/user-name.js +3 -0
  90. package/package.json +14 -13
@@ -8,7 +8,6 @@ const lodash_1 = require("lodash");
8
8
  const nanoid_1 = require("nanoid");
9
9
  const uuid_validate_1 = __importDefault(require("uuid-validate"));
10
10
  const exceptions_1 = require("../exceptions");
11
- const apply_function_to_column_name_1 = require("./apply-function-to-column-name");
12
11
  const get_column_1 = require("./get-column");
13
12
  const get_relation_type_1 = require("./get-relation-type");
14
13
  const helpers_1 = require("../database/helpers");
@@ -44,7 +43,7 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
44
43
  applySearch(schema, dbQuery, query.search, collection);
45
44
  }
46
45
  if (query.group) {
47
- dbQuery.groupBy(`${collection}.${query.group.map(apply_function_to_column_name_1.applyFunctionToColumnName)}`);
46
+ dbQuery.groupBy(query.group.map((column) => (0, get_column_1.getColumn)(knex, collection, column, false)));
48
47
  }
49
48
  if (query.aggregate) {
50
49
  applyAggregate(dbQuery, query.aggregate, collection);
@@ -77,44 +76,44 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
77
76
  return dbQuery;
78
77
  }
79
78
  exports.default = applyQuery;
80
- /**
81
- * Apply a given filter object to the Knex QueryBuilder instance.
82
- *
83
- * Relational nested filters, like the following example:
84
- *
85
- * ```json
86
- * // Fetch pages that have articles written by Rijk
87
- *
88
- * {
89
- * "articles": {
90
- * "author": {
91
- * "name": {
92
- * "_eq": "Rijk"
93
- * }
94
- * }
95
- * }
96
- * }
97
- * ```
98
- *
99
- * are handled by joining the nested tables, and using a where statement on the top level on the
100
- * nested field through the join. This allows us to filter the top level items based on nested data.
101
- * The where on the root is done with a subquery to prevent duplicates, any nested joins are done
102
- * with aliases to prevent naming conflicts.
103
- *
104
- * The output SQL for the above would look something like:
105
- *
106
- * ```sql
107
- * SELECT *
108
- * FROM pages
109
- * WHERE
110
- * pages.id in (
111
- * SELECT articles.page_id AS page_id
112
- * FROM articles
113
- * LEFT JOIN authors AS xviqp ON articles.author = xviqp.id
114
- * WHERE xviqp.name = 'Rijk'
115
- * )
116
- * ```
117
- */
79
+ function getRelationInfo(relations, collection, field) {
80
+ var _a, _b;
81
+ const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
82
+ if (implicitRelation) {
83
+ if (implicitRelation[2] === undefined) {
84
+ const [m2oCollection, m2oField] = implicitRelation;
85
+ const relation = {
86
+ collection: m2oCollection,
87
+ field: m2oField,
88
+ related_collection: collection,
89
+ schema: null,
90
+ meta: null,
91
+ };
92
+ return { relation, relationType: 'o2m' };
93
+ }
94
+ else {
95
+ const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
96
+ const relation = {
97
+ collection: a2oCollection,
98
+ field: a2oItemField,
99
+ related_collection: collection,
100
+ schema: null,
101
+ meta: {
102
+ one_collection_field: a2oCollectionField,
103
+ one_field: field,
104
+ },
105
+ };
106
+ return { relation, relationType: 'o2a' };
107
+ }
108
+ }
109
+ const relation = (_b = relations.find((relation) => {
110
+ var _a;
111
+ return ((relation.collection === collection && relation.field === field) ||
112
+ (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
113
+ })) !== null && _b !== void 0 ? _b : null;
114
+ const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
115
+ return { relation, relationType };
116
+ }
118
117
  function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
119
118
  const helpers = (0, helpers_1.getHelpers)(knex);
120
119
  const relations = schema.relations;
@@ -144,31 +143,34 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
144
143
  followRelation(path);
145
144
  function followRelation(pathParts, parentCollection = collection, parentAlias) {
146
145
  /**
147
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
146
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
148
147
  */
149
148
  const pathRoot = pathParts[0].split(':')[0];
150
- const relation = relations.find((relation) => {
151
- var _a;
152
- return ((relation.collection === parentCollection && relation.field === pathRoot) ||
153
- (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
154
- });
155
- if (!relation)
149
+ const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
150
+ if (!relation) {
156
151
  return;
157
- const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
152
+ }
158
153
  const alias = generateAlias();
159
154
  (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
160
155
  if (relationType === 'm2o') {
161
156
  dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
162
157
  }
163
- if (relationType === 'm2a') {
158
+ if (relationType === 'a2o') {
164
159
  const pathScope = pathParts[0].split(':')[1];
165
160
  if (!pathScope) {
166
161
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
167
162
  }
168
163
  dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
169
164
  joinClause
170
- .on(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`))
171
- .andOnVal(relation.meta.one_collection_field, '=', pathScope);
165
+ .onVal(relation.meta.one_collection_field, '=', pathScope)
166
+ .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
167
+ });
168
+ }
169
+ if (relationType === 'o2a') {
170
+ dbQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
171
+ joinClause
172
+ .onVal(relation.meta.one_collection_field, '=', parentCollection)
173
+ .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
172
174
  });
173
175
  }
174
176
  // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
@@ -180,7 +182,7 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
180
182
  if (relationType === 'm2o') {
181
183
  parent = relation.related_collection;
182
184
  }
183
- else if (relationType === 'm2a') {
185
+ else if (relationType === 'a2o') {
184
186
  const pathScope = pathParts[0].split(':')[1];
185
187
  if (!pathScope) {
186
188
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
@@ -216,17 +218,12 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
216
218
  }
217
219
  const filterPath = getFilterPath(key, value);
218
220
  /**
219
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
221
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
220
222
  */
221
223
  const pathRoot = filterPath[0].split(':')[0];
222
- const relation = relations.find((relation) => {
223
- var _a;
224
- return ((relation.collection === collection && relation.field === pathRoot) ||
225
- (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
226
- });
224
+ const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
227
225
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
228
- const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection: collection, field: pathRoot }) : null;
229
- if (relationType === 'm2o' || relationType === 'm2a' || relationType === null) {
226
+ if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
230
227
  if (filterPath.length > 1) {
231
228
  const columnName = getWhereColumn(filterPath, collection);
232
229
  if (!columnName)
@@ -238,7 +235,13 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
238
235
  }
239
236
  }
240
237
  else if (subQuery === false) {
241
- const pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
238
+ if (!relation)
239
+ continue;
240
+ let pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
241
+ if (relationType === 'o2a') {
242
+ pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
243
+ }
244
+ // Note: knex's types don't appreciate knex.raw in whereIn, even though it's officially supported
242
245
  dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
243
246
  const field = relation.field;
244
247
  const collection = relation.collection;
@@ -377,22 +380,17 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
377
380
  return followRelation(path);
378
381
  function followRelation(pathParts, parentCollection = collection, parentAlias) {
379
382
  /**
380
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
383
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
381
384
  */
382
385
  const pathRoot = pathParts[0].split(':')[0];
383
- const relation = relations.find((relation) => {
384
- var _a;
385
- return ((relation.collection === parentCollection && relation.field === pathRoot) ||
386
- (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
387
- });
386
+ const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
388
387
  if (!relation) {
389
388
  throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
390
389
  }
391
- const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
392
390
  const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
393
391
  const remainingParts = pathParts.slice(1);
394
392
  let parent;
395
- if (relationType === 'm2a') {
393
+ if (relationType === 'a2o') {
396
394
  const pathScope = pathParts[0].split(':')[1];
397
395
  if (!pathScope) {
398
396
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
@@ -10,6 +10,7 @@ const database_1 = __importDefault(require("../database"));
10
10
  const get_schema_1 = require("./get-schema");
11
11
  const services_1 = require("../services");
12
12
  const lodash_1 = require("lodash");
13
+ const logger_1 = __importDefault(require("../logger"));
13
14
  async function applySnapshot(snapshot, options) {
14
15
  var _a, _b, _c, _d;
15
16
  const database = (_a = options === null || options === void 0 ? void 0 : options.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
@@ -20,7 +21,13 @@ async function applySnapshot(snapshot, options) {
20
21
  const collectionsService = new services_1.CollectionsService({ knex: trx, schema });
21
22
  for (const { collection, diff } of snapshotDiff.collections) {
22
23
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
23
- await collectionsService.deleteOne(collection);
24
+ try {
25
+ await collectionsService.deleteOne(collection);
26
+ }
27
+ catch (err) {
28
+ logger_1.default.error(`Failed to delete collection "${collection}"`);
29
+ throw err;
30
+ }
24
31
  }
25
32
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && diff[0].rhs) {
26
33
  // We'll nest the to-be-created fields in the same collection creation, to prevent
@@ -28,10 +35,16 @@ async function applySnapshot(snapshot, options) {
28
35
  const fields = snapshotDiff.fields
29
36
  .filter((fieldDiff) => fieldDiff.collection === collection)
30
37
  .map((fieldDiff) => fieldDiff.diff[0].rhs);
31
- await collectionsService.createOne({
32
- ...diff[0].rhs,
33
- fields,
34
- });
38
+ try {
39
+ await collectionsService.createOne({
40
+ ...diff[0].rhs,
41
+ fields,
42
+ });
43
+ }
44
+ catch (err) {
45
+ logger_1.default.error(`Failed to create collection "${collection}"`);
46
+ throw err;
47
+ }
35
48
  // Now that the fields are in for this collection, we can strip them from the field
36
49
  // edits
37
50
  snapshotDiff.fields = snapshotDiff.fields.filter((fieldDiff) => fieldDiff.collection !== collection);
@@ -41,27 +54,51 @@ async function applySnapshot(snapshot, options) {
41
54
  return field.collection === collection;
42
55
  });
43
56
  if (newValues) {
44
- await collectionsService.updateOne(collection, newValues);
57
+ try {
58
+ await collectionsService.updateOne(collection, newValues);
59
+ }
60
+ catch (err) {
61
+ logger_1.default.error(`Failed to update collection "${collection}"`);
62
+ throw err;
63
+ }
45
64
  }
46
65
  }
47
66
  }
48
67
  const fieldsService = new services_1.FieldsService({ knex: trx, schema: await (0, get_schema_1.getSchema)({ database: trx }) });
49
68
  for (const { collection, field, diff } of snapshotDiff.fields) {
50
69
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N') {
51
- await fieldsService.createField(collection, diff[0].rhs);
70
+ try {
71
+ await fieldsService.createField(collection, diff[0].rhs);
72
+ }
73
+ catch (err) {
74
+ logger_1.default.error(`Failed to create field "${collection}.${field}"`);
75
+ throw err;
76
+ }
52
77
  }
53
78
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
54
79
  const newValues = snapshot.fields.find((snapshotField) => {
55
80
  return snapshotField.collection === collection && snapshotField.field === field;
56
81
  });
57
82
  if (newValues) {
58
- await fieldsService.updateField(collection, {
59
- ...newValues,
60
- });
83
+ try {
84
+ await fieldsService.updateField(collection, {
85
+ ...newValues,
86
+ });
87
+ }
88
+ catch (err) {
89
+ logger_1.default.error(`Failed to update field "${collection}.${field}"`);
90
+ throw err;
91
+ }
61
92
  }
62
93
  }
63
94
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
64
- await fieldsService.deleteField(collection, field);
95
+ try {
96
+ await fieldsService.deleteField(collection, field);
97
+ }
98
+ catch (err) {
99
+ logger_1.default.error(`Failed to delete field "${collection}.${field}"`);
100
+ throw err;
101
+ }
65
102
  // Field deletion also cleans up the relationship. We should ignore any relationship
66
103
  // changes attached to this now non-existing field
67
104
  snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection && relation.field === field) === false);
@@ -74,18 +111,36 @@ async function applySnapshot(snapshot, options) {
74
111
  (0, lodash_1.set)(structure, diffEdit.path, undefined);
75
112
  }
76
113
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N') {
77
- await relationsService.createOne(diff[0].rhs);
114
+ try {
115
+ await relationsService.createOne(diff[0].rhs);
116
+ }
117
+ catch (err) {
118
+ logger_1.default.error(`Failed to create relation "${collection}.${field}"`);
119
+ throw err;
120
+ }
78
121
  }
79
122
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
80
123
  const newValues = snapshot.relations.find((relation) => {
81
124
  return relation.collection === collection && relation.field === field;
82
125
  });
83
126
  if (newValues) {
84
- await relationsService.updateOne(collection, field, newValues);
127
+ try {
128
+ await relationsService.updateOne(collection, field, newValues);
129
+ }
130
+ catch (err) {
131
+ logger_1.default.error(`Failed to update relation "${collection}.${field}"`);
132
+ throw err;
133
+ }
85
134
  }
86
135
  }
87
136
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
88
- await relationsService.deleteOne(collection, field);
137
+ try {
138
+ await relationsService.deleteOne(collection, field);
139
+ }
140
+ catch (err) {
141
+ logger_1.default.error(`Failed to delete relation "${collection}.${field}"`);
142
+ throw err;
143
+ }
89
144
  }
90
145
  }
91
146
  });
@@ -88,7 +88,7 @@ async function getASTFromQuery(collection, query, schema, options) {
88
88
  const parts = name.split('.');
89
89
  let rootField = parts[0];
90
90
  let collectionScope = null;
91
- // m2a related collection scoped field selector `fields=sections.section_id:headings.title`
91
+ // a2o related collection scoped field selector `fields=sections.section_id:headings.title`
92
92
  if (rootField.includes(':')) {
93
93
  const [key, scope] = rootField.split(':');
94
94
  rootField = key;
@@ -136,14 +136,14 @@ async function getASTFromQuery(collection, query, schema, options) {
136
136
  if (!relationType)
137
137
  continue;
138
138
  let child = null;
139
- if (relationType === 'm2a') {
139
+ if (relationType === 'a2o') {
140
140
  const allowedCollections = relation.meta.one_allowed_collections.filter((collection) => {
141
141
  if (!permissions)
142
142
  return true;
143
143
  return permissions.some((permission) => permission.collection === collection);
144
144
  });
145
145
  child = {
146
- type: 'm2a',
146
+ type: 'a2o',
147
147
  names: allowedCollections,
148
148
  children: {},
149
149
  query: {},
@@ -1,3 +1,3 @@
1
- import { Accountability } from '@directus/shared/types';
1
+ import { Permission, Accountability } from '@directus/shared/types';
2
2
  import { SchemaOverview } from '../types';
3
- export declare function getPermissions(accountability: Accountability, schema: SchemaOverview): Promise<any>;
3
+ export declare function getPermissions(accountability: Accountability, schema: SchemaOverview): Promise<Permission[]>;
@@ -9,6 +9,7 @@ const lodash_1 = require("lodash");
9
9
  const database_1 = __importDefault(require("../database"));
10
10
  const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
11
11
  const merge_permissions_1 = require("../utils/merge-permissions");
12
+ const merge_permissions_for_share_1 = require("./merge-permissions-for-share");
12
13
  const users_1 = require("../services/users");
13
14
  const roles_1 = require("../services/roles");
14
15
  const cache_1 = require("../cache");
@@ -16,91 +17,135 @@ const object_hash_1 = __importDefault(require("object-hash"));
16
17
  const env_1 = __importDefault(require("../env"));
17
18
  async function getPermissions(accountability, schema) {
18
19
  const database = (0, database_1.default)();
19
- const { systemCache } = (0, cache_1.getCache)();
20
+ const { systemCache, cache } = (0, cache_1.getCache)();
20
21
  let permissions = [];
21
- const { user, role, app, admin } = accountability;
22
- const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin })}`;
22
+ const { user, role, app, admin, share_scope } = accountability;
23
+ const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin, share_scope })}`;
23
24
  if (env_1.default.CACHE_PERMISSIONS !== false) {
24
25
  const cachedPermissions = await systemCache.get(cacheKey);
25
26
  if (cachedPermissions) {
26
- return cachedPermissions;
27
- }
28
- }
29
- if (accountability.admin !== true) {
30
- const permissionsForRole = await database
31
- .select('*')
32
- .from('directus_permissions')
33
- .where({ role: accountability.role });
34
- const requiredPermissionData = {
35
- $CURRENT_USER: [],
36
- $CURRENT_ROLE: [],
37
- };
38
- permissions = permissionsForRole.map((permissionRaw) => {
39
- const permission = (0, lodash_1.cloneDeep)(permissionRaw);
40
- if (permission.permissions && typeof permission.permissions === 'string') {
41
- permission.permissions = JSON.parse(permission.permissions);
42
- }
43
- else if (permission.permissions === null) {
44
- permission.permissions = {};
45
- }
46
- if (permission.validation && typeof permission.validation === 'string') {
47
- permission.validation = JSON.parse(permission.validation);
48
- }
49
- else if (permission.validation === null) {
50
- permission.validation = {};
51
- }
52
- if (permission.presets && typeof permission.presets === 'string') {
53
- permission.presets = JSON.parse(permission.presets);
54
- }
55
- else if (permission.presets === null) {
56
- permission.presets = {};
57
- }
58
- if (permission.fields && typeof permission.fields === 'string') {
59
- permission.fields = permission.fields.split(',');
27
+ if (!cachedPermissions.containDynamicData) {
28
+ return processPermissions(accountability, cachedPermissions.permissions, {});
60
29
  }
61
- else if (permission.fields === null) {
62
- permission.fields = [];
30
+ const cachedFilterContext = await (cache === null || cache === void 0 ? void 0 : cache.get(`filterContext-${(0, object_hash_1.default)({ user, role, permissions: cachedPermissions.permissions })}`));
31
+ if (cachedFilterContext) {
32
+ return processPermissions(accountability, cachedPermissions.permissions, cachedFilterContext);
63
33
  }
64
- const extractPermissionData = (val) => {
65
- if (typeof val === 'string' && val.startsWith('$CURRENT_USER.')) {
66
- requiredPermissionData.$CURRENT_USER.push(val.replace('$CURRENT_USER.', ''));
34
+ else {
35
+ const { permissions: parsedPermissions, requiredPermissionData, containDynamicData, } = parsePermissions(cachedPermissions.permissions);
36
+ permissions = parsedPermissions;
37
+ const filterContext = containDynamicData
38
+ ? await getFilterContext(schema, accountability, requiredPermissionData)
39
+ : {};
40
+ if (containDynamicData && env_1.default.CACHE_ENABLED !== false) {
41
+ await (cache === null || cache === void 0 ? void 0 : cache.set(`filterContext-${(0, object_hash_1.default)({ user, role, permissions })}`, filterContext));
67
42
  }
68
- if (typeof val === 'string' && val.startsWith('$CURRENT_ROLE.')) {
69
- requiredPermissionData.$CURRENT_ROLE.push(val.replace('$CURRENT_ROLE.', ''));
70
- }
71
- return val;
72
- };
73
- (0, utils_1.deepMap)(permission.permissions, extractPermissionData);
74
- (0, utils_1.deepMap)(permission.validation, extractPermissionData);
75
- (0, utils_1.deepMap)(permission.presets, extractPermissionData);
76
- return permission;
77
- });
78
- if (accountability.app === true) {
79
- permissions = (0, merge_permissions_1.mergePermissions)(permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
43
+ return processPermissions(accountability, permissions, filterContext);
44
+ }
45
+ }
46
+ }
47
+ if (accountability.admin !== true) {
48
+ const query = database.select('*').from('directus_permissions');
49
+ if (accountability.role) {
50
+ query.where({ role: accountability.role });
80
51
  }
81
- const usersService = new users_1.UsersService({ schema });
82
- const rolesService = new roles_1.RolesService({ schema });
83
- const filterContext = {};
84
- if (accountability.user && requiredPermissionData.$CURRENT_USER.length > 0) {
85
- filterContext.$CURRENT_USER = await usersService.readOne(accountability.user, {
86
- fields: requiredPermissionData.$CURRENT_USER,
87
- });
52
+ else {
53
+ query.whereNull('role');
88
54
  }
89
- if (accountability.role && requiredPermissionData.$CURRENT_ROLE.length > 0) {
90
- filterContext.$CURRENT_ROLE = await rolesService.readOne(accountability.role, {
91
- fields: requiredPermissionData.$CURRENT_ROLE,
92
- });
55
+ const permissionsForRole = await query;
56
+ const { permissions: parsedPermissions, requiredPermissionData, containDynamicData, } = parsePermissions(permissionsForRole);
57
+ permissions = parsedPermissions;
58
+ if (accountability.app === true) {
59
+ permissions = (0, merge_permissions_1.mergePermissions)('or', permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
93
60
  }
94
- permissions = permissions.map((permission) => {
95
- permission.permissions = (0, utils_1.parseFilter)(permission.permissions, accountability, filterContext);
96
- permission.validation = (0, utils_1.parseFilter)(permission.validation, accountability, filterContext);
97
- permission.presets = (0, utils_1.parseFilter)(permission.presets, accountability, filterContext);
98
- return permission;
99
- });
61
+ if (accountability.share_scope) {
62
+ permissions = (0, merge_permissions_for_share_1.mergePermissionsForShare)(permissions, accountability, schema);
63
+ }
64
+ const filterContext = containDynamicData
65
+ ? await getFilterContext(schema, accountability, requiredPermissionData)
66
+ : {};
100
67
  if (env_1.default.CACHE_PERMISSIONS !== false) {
101
- await systemCache.set(cacheKey, permissions);
68
+ await systemCache.set(cacheKey, { permissions, containDynamicData });
69
+ if (containDynamicData && env_1.default.CACHE_ENABLED !== false) {
70
+ await (cache === null || cache === void 0 ? void 0 : cache.set(`filterContext-${(0, object_hash_1.default)({ user, role, permissions })}`, filterContext));
71
+ }
102
72
  }
73
+ return processPermissions(accountability, permissions, filterContext);
103
74
  }
104
75
  return permissions;
105
76
  }
106
77
  exports.getPermissions = getPermissions;
78
+ function parsePermissions(permissions) {
79
+ const requiredPermissionData = {
80
+ $CURRENT_USER: [],
81
+ $CURRENT_ROLE: [],
82
+ };
83
+ let containDynamicData = false;
84
+ permissions = permissions.map((permissionRaw) => {
85
+ const permission = (0, lodash_1.cloneDeep)(permissionRaw);
86
+ if (permission.permissions && typeof permission.permissions === 'string') {
87
+ permission.permissions = JSON.parse(permission.permissions);
88
+ }
89
+ else if (permission.permissions === null) {
90
+ permission.permissions = {};
91
+ }
92
+ if (permission.validation && typeof permission.validation === 'string') {
93
+ permission.validation = JSON.parse(permission.validation);
94
+ }
95
+ else if (permission.validation === null) {
96
+ permission.validation = {};
97
+ }
98
+ if (permission.presets && typeof permission.presets === 'string') {
99
+ permission.presets = JSON.parse(permission.presets);
100
+ }
101
+ else if (permission.presets === null) {
102
+ permission.presets = {};
103
+ }
104
+ if (permission.fields && typeof permission.fields === 'string') {
105
+ permission.fields = permission.fields.split(',');
106
+ }
107
+ else if (permission.fields === null) {
108
+ permission.fields = [];
109
+ }
110
+ const extractPermissionData = (val) => {
111
+ if (typeof val === 'string' && val.startsWith('$CURRENT_USER.')) {
112
+ requiredPermissionData.$CURRENT_USER.push(val.replace('$CURRENT_USER.', ''));
113
+ containDynamicData = true;
114
+ }
115
+ if (typeof val === 'string' && val.startsWith('$CURRENT_ROLE.')) {
116
+ requiredPermissionData.$CURRENT_ROLE.push(val.replace('$CURRENT_ROLE.', ''));
117
+ containDynamicData = true;
118
+ }
119
+ return val;
120
+ };
121
+ (0, utils_1.deepMap)(permission.permissions, extractPermissionData);
122
+ (0, utils_1.deepMap)(permission.validation, extractPermissionData);
123
+ (0, utils_1.deepMap)(permission.presets, extractPermissionData);
124
+ return permission;
125
+ });
126
+ return { permissions, requiredPermissionData, containDynamicData };
127
+ }
128
+ async function getFilterContext(schema, accountability, requiredPermissionData) {
129
+ const usersService = new users_1.UsersService({ schema });
130
+ const rolesService = new roles_1.RolesService({ schema });
131
+ const filterContext = {};
132
+ if (accountability.user && requiredPermissionData.$CURRENT_USER.length > 0) {
133
+ filterContext.$CURRENT_USER = await usersService.readOne(accountability.user, {
134
+ fields: requiredPermissionData.$CURRENT_USER,
135
+ });
136
+ }
137
+ if (accountability.role && requiredPermissionData.$CURRENT_ROLE.length > 0) {
138
+ filterContext.$CURRENT_ROLE = await rolesService.readOne(accountability.role, {
139
+ fields: requiredPermissionData.$CURRENT_ROLE,
140
+ });
141
+ }
142
+ return filterContext;
143
+ }
144
+ function processPermissions(accountability, permissions, filterContext) {
145
+ return permissions.map((permission) => {
146
+ permission.permissions = (0, utils_1.parseFilter)(permission.permissions, accountability, filterContext);
147
+ permission.validation = (0, utils_1.parseFilter)(permission.validation, accountability, filterContext);
148
+ permission.presets = (0, utils_1.parseFilter)(permission.presets, accountability, filterContext);
149
+ return permission;
150
+ });
151
+ }
@@ -3,4 +3,4 @@ export declare function getRelationType(getRelationOptions: {
3
3
  relation: Relation;
4
4
  collection: string | null;
5
5
  field: string;
6
- }): 'm2o' | 'o2m' | 'm2a' | null;
6
+ }): 'm2o' | 'o2m' | 'a2o' | null;
@@ -10,7 +10,7 @@ function getRelationType(getRelationOptions) {
10
10
  relation.field === field &&
11
11
  ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_collection_field) &&
12
12
  ((_b = relation.meta) === null || _b === void 0 ? void 0 : _b.one_allowed_collections)) {
13
- return 'm2a';
13
+ return 'a2o';
14
14
  }
15
15
  if (relation.collection === collection && relation.field === field) {
16
16
  return 'm2o';
@@ -0,0 +1,5 @@
1
+ import { Permission, Accountability, Filter } from '@directus/shared/types';
2
+ import { SchemaOverview } from '../types';
3
+ export declare function mergePermissionsForShare(currentPermissions: Permission[], accountability: Accountability, schema: SchemaOverview): Permission[];
4
+ export declare function traverse(schema: SchemaOverview, rootItemPrimaryKeyField: string, rootItemPrimaryKey: string, currentCollection: string, parentCollections?: string[], path?: string[]): Partial<Permission>[];
5
+ export declare function getFilterForPath(type: 'o2m' | 'm2o' | 'a2o', path: string[], rootPrimaryKeyField: string, rootPrimaryKey: string): Filter;