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 +1 -1
- package/dist/app.js +3 -0
- package/dist/auth/drivers/oauth2.js +2 -2
- package/dist/auth/drivers/openid.js +2 -2
- package/dist/env.js +4 -0
- package/dist/services/graphql.js +12 -1
- package/dist/services/import-export.js +2 -0
- package/dist/services/items.js +7 -2
- package/dist/services/payload.js +1 -1
- package/dist/utils/apply-query.d.ts +2 -1
- package/dist/utils/apply-query.js +117 -149
- package/dist/utils/get-column-path.d.ts +16 -0
- package/dist/utils/get-column-path.js +46 -0
- package/dist/utils/get-relation-info.d.ts +7 -0
- package/dist/utils/get-relation-info.js +45 -0
- package/dist/utils/get-relation-type.d.ts +1 -1
- package/dist/utils/merge-permissions-for-share.js +1 -1
- package/dist/utils/reduce-schema.js +4 -5
- package/package.json +16 -18
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
|
|
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 &&
|
|
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
|
}
|
package/dist/services/graphql.js
CHANGED
|
@@ -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,
|
|
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) => {
|
package/dist/services/items.js
CHANGED
|
@@ -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
|
|
94
|
-
|
|
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);
|
package/dist/services/payload.js
CHANGED
|
@@ -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
|
|
13
|
-
const
|
|
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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 =
|
|
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;
|
|
@@ -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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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.
|
|
82
|
-
"@directus/drive": "9.
|
|
83
|
-
"@directus/drive-azure": "9.
|
|
84
|
-
"@directus/drive-gcs": "9.
|
|
85
|
-
"@directus/drive-s3": "9.
|
|
86
|
-
"@directus/extensions-sdk": "9.
|
|
87
|
-
"@directus/format-title": "9.
|
|
88
|
-
"@directus/schema": "9.
|
|
89
|
-
"@directus/shared": "9.
|
|
90
|
-
"@directus/specs": "9.
|
|
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.
|
|
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.
|
|
171
|
+
"sqlite3": "^5.0.6",
|
|
174
172
|
"tedious": "^13.0.0"
|
|
175
173
|
},
|
|
176
|
-
"gitHead": "
|
|
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.
|
|
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"
|