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.
- package/dist/app.js +5 -3
- package/dist/auth/auth.d.ts +4 -6
- package/dist/auth/auth.js +5 -9
- package/dist/auth/drivers/ldap.d.ts +3 -3
- package/dist/auth/drivers/ldap.js +11 -4
- package/dist/auth/drivers/local.d.ts +2 -2
- package/dist/auth/drivers/local.js +5 -12
- package/dist/auth/drivers/oauth2.d.ts +4 -4
- package/dist/auth/drivers/oauth2.js +47 -21
- package/dist/auth/drivers/openid.d.ts +4 -4
- package/dist/auth/drivers/openid.js +35 -19
- package/dist/cli/commands/bootstrap/index.js +3 -2
- package/dist/cli/commands/init/index.js +3 -7
- package/dist/cli/commands/schema/apply.js +1 -1
- package/dist/cli/utils/defaults.d.ts +11 -0
- package/dist/cli/utils/defaults.js +14 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +16 -2
- package/dist/controllers/shares.d.ts +2 -0
- package/dist/controllers/shares.js +212 -0
- package/dist/controllers/users.js +21 -9
- package/dist/database/migrations/20211211A-add-shares.d.ts +3 -0
- package/dist/database/migrations/20211211A-add-shares.js +37 -0
- package/dist/database/run-ast.js +5 -5
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -15
- package/dist/database/system-data/app-access-permissions/index.d.ts +1 -0
- package/dist/database/system-data/app-access-permissions/index.js +4 -2
- package/dist/database/system-data/app-access-permissions/schema-access-permissions.yaml +17 -0
- package/dist/database/system-data/collections/collections.yaml +3 -0
- package/dist/database/system-data/fields/sessions.yaml +1 -1
- package/dist/database/system-data/fields/shares.yaml +73 -0
- package/dist/database/system-data/fields/users.yaml +1 -1
- package/dist/database/system-data/relations/relations.yaml +15 -0
- package/dist/emitter.d.ts +3 -2
- package/dist/emitter.js +13 -6
- package/dist/exceptions/index.d.ts +2 -0
- package/dist/exceptions/index.js +2 -0
- package/dist/exceptions/invalid-token.d.ts +4 -0
- package/dist/exceptions/invalid-token.js +10 -0
- package/dist/exceptions/unexpected-response.d.ts +4 -0
- package/dist/exceptions/unexpected-response.js +10 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +10 -4
- package/dist/middleware/authenticate.js +5 -15
- package/dist/middleware/check-ip.js +9 -6
- package/dist/middleware/respond.js +4 -1
- package/dist/services/activity.d.ts +2 -1
- package/dist/services/activity.js +2 -2
- package/dist/services/authentication.d.ts +2 -7
- package/dist/services/authentication.js +81 -41
- package/dist/services/authorization.js +3 -3
- package/dist/services/collections.d.ts +1 -2
- package/dist/services/collections.js +2 -2
- package/dist/services/files.d.ts +2 -2
- package/dist/services/files.js +14 -8
- package/dist/services/graphql.js +16 -5
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.d.ts +1 -15
- package/dist/services/notifications.d.ts +2 -2
- package/dist/services/permissions.d.ts +2 -2
- package/dist/services/roles.d.ts +2 -2
- package/dist/services/shares.d.ts +17 -0
- package/dist/services/shares.js +135 -0
- package/dist/services/specifications.js +1 -1
- package/dist/services/users.d.ts +2 -2
- package/dist/services/users.js +8 -6
- package/dist/services/webhooks.d.ts +2 -2
- package/dist/tests/database/migrations/run.test.d.ts +1 -0
- package/dist/tests/database/migrations/run.test.js +29 -0
- package/dist/types/ast.d.ts +3 -3
- package/dist/types/auth.d.ts +31 -0
- package/dist/types/extensions.d.ts +2 -0
- package/dist/types/items.d.ts +14 -0
- package/dist/utils/apply-query.d.ts +0 -38
- package/dist/utils/apply-query.js +67 -69
- package/dist/utils/apply-snapshot.js +69 -14
- package/dist/utils/get-ast-from-query.js +3 -3
- package/dist/utils/get-permissions.d.ts +2 -2
- package/dist/utils/get-permissions.js +117 -72
- package/dist/utils/get-relation-type.d.ts +1 -1
- package/dist/utils/get-relation-type.js +1 -1
- package/dist/utils/merge-permissions-for-share.d.ts +5 -0
- package/dist/utils/merge-permissions-for-share.js +116 -0
- package/dist/utils/merge-permissions.d.ts +13 -1
- package/dist/utils/merge-permissions.js +29 -21
- package/dist/utils/reduce-schema.d.ts +2 -2
- package/dist/utils/reduce-schema.js +7 -7
- package/dist/utils/user-name.js +3 -0
- 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(
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
151
|
-
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
.
|
|
171
|
-
.
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 === '
|
|
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: '
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
|
@@ -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 '
|
|
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;
|