directus 9.9.1 → 9.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app.js +3 -0
- package/dist/auth/drivers/oauth2.d.ts +1 -1
- package/dist/auth/drivers/oauth2.js +14 -11
- package/dist/auth/drivers/openid.d.ts +1 -1
- package/dist/auth/drivers/openid.js +14 -11
- package/dist/cli/commands/schema/apply.js +4 -3
- package/dist/controllers/assets.js +8 -9
- package/dist/database/helpers/date/dialects/sqlite.js +6 -2
- package/dist/database/index.js +5 -0
- package/dist/database/migrations/20210225A-add-relations-sort-field.js +2 -1
- package/dist/database/migrations/20210506A-rename-interfaces.js +2 -1
- package/dist/database/migrations/20210802A-replace-groups.js +2 -1
- package/dist/database/migrations/20210805A-update-groups.js +2 -1
- package/dist/database/migrations/20210805B-change-image-metadata-structure.js +3 -2
- package/dist/database/migrations/20211007A-update-presets.js +5 -4
- package/dist/database/run-ast.js +10 -14
- package/dist/database/system-data/fields/activity.yaml +3 -0
- package/dist/database/system-data/fields/dashboards.yaml +3 -1
- package/dist/database/system-data/fields/notifications.yaml +3 -1
- package/dist/database/system-data/fields/panels.yaml +3 -1
- package/dist/database/system-data/fields/shares.yaml +3 -1
- package/dist/env.js +193 -10
- package/dist/exceptions/index.d.ts +1 -0
- package/dist/exceptions/index.js +1 -0
- package/dist/exceptions/invalid-provider.d.ts +4 -0
- package/dist/exceptions/invalid-provider.js +10 -0
- package/dist/exceptions/range-not-satisfiable.d.ts +2 -2
- package/dist/exceptions/range-not-satisfiable.js +5 -1
- package/dist/middleware/graphql.js +2 -1
- package/dist/services/assets.js +27 -1
- package/dist/services/authentication.js +4 -1
- package/dist/services/fields.js +15 -8
- package/dist/services/graphql.js +61 -33
- package/dist/services/import-export.d.ts +1 -1
- package/dist/services/import-export.js +14 -11
- package/dist/services/items.d.ts +3 -3
- package/dist/services/items.js +31 -3
- package/dist/services/payload.d.ts +2 -2
- package/dist/services/payload.js +9 -8
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +20 -0
- package/dist/utils/apply-query.d.ts +2 -1
- package/dist/utils/apply-query.js +144 -149
- package/dist/utils/apply-snapshot.d.ts +3 -3
- package/dist/utils/apply-snapshot.js +64 -49
- package/dist/utils/get-ast-from-query.js +1 -7
- package/dist/utils/get-column-path.d.ts +16 -0
- package/dist/utils/get-column-path.js +46 -0
- package/dist/utils/get-default-value.js +4 -3
- package/dist/utils/get-permissions.d.ts +1 -1
- package/dist/utils/get-permissions.js +9 -8
- 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/get-schema.js +2 -1
- package/dist/utils/get-snapshot.js +22 -4
- package/dist/utils/merge-permissions-for-share.js +1 -1
- package/dist/utils/parse-json.d.ts +5 -0
- package/dist/utils/parse-json.js +19 -0
- package/dist/utils/reduce-schema.js +4 -5
- package/dist/utils/sanitize-query.d.ts +1 -2
- package/dist/utils/sanitize-query.js +6 -5
- package/dist/utils/validate-keys.d.ts +6 -0
- package/dist/utils/validate-keys.js +28 -0
- package/dist/utils/validate-query.js +1 -1
- package/package.json +16 -18
package/dist/services/items.js
CHANGED
|
@@ -15,8 +15,9 @@ const translate_1 = require("../exceptions/database/translate");
|
|
|
15
15
|
const types_1 = require("../types");
|
|
16
16
|
const get_ast_from_query_1 = __importDefault(require("../utils/get-ast-from-query"));
|
|
17
17
|
const authorization_1 = require("./authorization");
|
|
18
|
-
const payload_1 = require("./payload");
|
|
19
18
|
const index_1 = require("./index");
|
|
19
|
+
const payload_1 = require("./payload");
|
|
20
|
+
const validate_keys_1 = require("../utils/validate-keys");
|
|
20
21
|
class ItemsService {
|
|
21
22
|
constructor(collection, options) {
|
|
22
23
|
this.collection = collection;
|
|
@@ -90,8 +91,13 @@ class ItemsService {
|
|
|
90
91
|
// In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
|
|
91
92
|
let primaryKey = payloadWithTypeCasting[primaryKeyField];
|
|
92
93
|
try {
|
|
93
|
-
const result = await trx
|
|
94
|
-
|
|
94
|
+
const result = await trx
|
|
95
|
+
.insert(payloadWithoutAliases)
|
|
96
|
+
.into(this.collection)
|
|
97
|
+
.returning(primaryKeyField)
|
|
98
|
+
.then((result) => result[0]);
|
|
99
|
+
const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
|
|
100
|
+
primaryKey = primaryKey !== null && primaryKey !== void 0 ? primaryKey : returnedKey;
|
|
95
101
|
}
|
|
96
102
|
catch (err) {
|
|
97
103
|
throw await (0, translate_1.translateDatabaseError)(err);
|
|
@@ -239,6 +245,7 @@ class ItemsService {
|
|
|
239
245
|
*/
|
|
240
246
|
async readOne(key, query = {}, opts) {
|
|
241
247
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
248
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
|
|
242
249
|
const filterWithKey = (0, lodash_1.assign)({}, query.filter, { [primaryKeyField]: { _eq: key } });
|
|
243
250
|
const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
|
|
244
251
|
const results = await this.readByQuery(queryWithKey, opts);
|
|
@@ -253,8 +260,13 @@ class ItemsService {
|
|
|
253
260
|
async readMany(keys, query = {}, opts) {
|
|
254
261
|
var _a;
|
|
255
262
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
263
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
|
|
256
264
|
const filterWithKey = { _and: [{ [primaryKeyField]: { _in: keys } }, (_a = query.filter) !== null && _a !== void 0 ? _a : {}] };
|
|
257
265
|
const queryWithKey = (0, lodash_1.assign)({}, query, { filter: filterWithKey });
|
|
266
|
+
// Set query limit as the number of keys
|
|
267
|
+
if (Array.isArray(keys) && keys.length > 0 && !queryWithKey.limit) {
|
|
268
|
+
queryWithKey.limit = keys.length;
|
|
269
|
+
}
|
|
258
270
|
const results = await this.readByQuery(queryWithKey, opts);
|
|
259
271
|
return results;
|
|
260
272
|
}
|
|
@@ -263,12 +275,16 @@ class ItemsService {
|
|
|
263
275
|
*/
|
|
264
276
|
async updateByQuery(query, data, opts) {
|
|
265
277
|
const keys = await this.getKeysByQuery(query);
|
|
278
|
+
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
279
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
|
|
266
280
|
return keys.length ? await this.updateMany(keys, data, opts) : [];
|
|
267
281
|
}
|
|
268
282
|
/**
|
|
269
283
|
* Update a single item by primary key
|
|
270
284
|
*/
|
|
271
285
|
async updateOne(key, data, opts) {
|
|
286
|
+
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
287
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
|
|
272
288
|
await this.updateMany([key], data, opts);
|
|
273
289
|
return key;
|
|
274
290
|
}
|
|
@@ -277,6 +293,7 @@ class ItemsService {
|
|
|
277
293
|
*/
|
|
278
294
|
async updateMany(keys, data, opts) {
|
|
279
295
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
296
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
|
|
280
297
|
const fields = Object.keys(this.schema.collections[this.collection].fields);
|
|
281
298
|
const aliases = Object.values(this.schema.collections[this.collection].fields)
|
|
282
299
|
.filter((field) => field.alias === true)
|
|
@@ -301,6 +318,8 @@ class ItemsService {
|
|
|
301
318
|
accountability: this.accountability,
|
|
302
319
|
})
|
|
303
320
|
: payload;
|
|
321
|
+
// Sort keys to ensure that the order is maintained
|
|
322
|
+
keys.sort();
|
|
304
323
|
if (this.accountability) {
|
|
305
324
|
await authorizationService.checkAccess('update', this.collection, keys);
|
|
306
325
|
}
|
|
@@ -404,6 +423,9 @@ class ItemsService {
|
|
|
404
423
|
async upsertOne(payload, opts) {
|
|
405
424
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
406
425
|
const primaryKey = payload[primaryKeyField];
|
|
426
|
+
if (primaryKey) {
|
|
427
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, primaryKey);
|
|
428
|
+
}
|
|
407
429
|
const exists = primaryKey &&
|
|
408
430
|
!!(await this.knex
|
|
409
431
|
.select(primaryKeyField)
|
|
@@ -444,12 +466,16 @@ class ItemsService {
|
|
|
444
466
|
*/
|
|
445
467
|
async deleteByQuery(query, opts) {
|
|
446
468
|
const keys = await this.getKeysByQuery(query);
|
|
469
|
+
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
470
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
|
|
447
471
|
return keys.length ? await this.deleteMany(keys, opts) : [];
|
|
448
472
|
}
|
|
449
473
|
/**
|
|
450
474
|
* Delete a single item by primary key
|
|
451
475
|
*/
|
|
452
476
|
async deleteOne(key, opts) {
|
|
477
|
+
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
478
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, key);
|
|
453
479
|
await this.deleteMany([key], opts);
|
|
454
480
|
return key;
|
|
455
481
|
}
|
|
@@ -458,10 +484,12 @@ class ItemsService {
|
|
|
458
484
|
*/
|
|
459
485
|
async deleteMany(keys, opts) {
|
|
460
486
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
487
|
+
(0, validate_keys_1.validateKeys)(this.schema, this.collection, primaryKeyField, keys);
|
|
461
488
|
if (this.accountability && this.accountability.admin !== true) {
|
|
462
489
|
const authorizationService = new authorization_1.AuthorizationService({
|
|
463
490
|
accountability: this.accountability,
|
|
464
491
|
schema: this.schema,
|
|
492
|
+
knex: this.knex,
|
|
465
493
|
});
|
|
466
494
|
await authorizationService.checkAccess('delete', this.collection, keys);
|
|
467
495
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Knex } from 'knex';
|
|
2
|
-
import { AbstractServiceOptions, Item, PrimaryKey } from '../types';
|
|
3
1
|
import { Accountability, SchemaOverview } from '@directus/shared/types';
|
|
2
|
+
import { Knex } from 'knex';
|
|
4
3
|
import { Helpers } from '../database/helpers';
|
|
4
|
+
import { AbstractServiceOptions, Item, PrimaryKey } from '../types';
|
|
5
5
|
declare type Action = 'create' | 'read' | 'update';
|
|
6
6
|
declare type Transformers = {
|
|
7
7
|
[type: string]: (context: {
|
package/dist/services/payload.js
CHANGED
|
@@ -4,18 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PayloadService = void 0;
|
|
7
|
+
const utils_1 = require("@directus/shared/utils");
|
|
7
8
|
const date_fns_1 = require("date-fns");
|
|
9
|
+
const flat_1 = require("flat");
|
|
8
10
|
const joi_1 = __importDefault(require("joi"));
|
|
9
11
|
const lodash_1 = require("lodash");
|
|
10
12
|
const uuid_1 = require("uuid");
|
|
13
|
+
const wellknown_1 = require("wellknown");
|
|
11
14
|
const database_1 = __importDefault(require("../database"));
|
|
12
|
-
const exceptions_1 = require("../exceptions");
|
|
13
|
-
const utils_1 = require("@directus/shared/utils");
|
|
14
|
-
const items_1 = require("./items");
|
|
15
|
-
const flat_1 = require("flat");
|
|
16
15
|
const helpers_1 = require("../database/helpers");
|
|
17
|
-
const
|
|
16
|
+
const exceptions_1 = require("../exceptions");
|
|
18
17
|
const generate_hash_1 = require("../utils/generate-hash");
|
|
18
|
+
const parse_json_1 = require("../utils/parse-json");
|
|
19
|
+
const items_1 = require("./items");
|
|
19
20
|
/**
|
|
20
21
|
* Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
|
|
21
22
|
* handled correctly.
|
|
@@ -55,7 +56,7 @@ class PayloadService {
|
|
|
55
56
|
if (action === 'read') {
|
|
56
57
|
if (typeof value === 'string') {
|
|
57
58
|
try {
|
|
58
|
-
return
|
|
59
|
+
return (0, parse_json_1.parseJSON)(value);
|
|
59
60
|
}
|
|
60
61
|
catch {
|
|
61
62
|
return value;
|
|
@@ -196,7 +197,7 @@ class PayloadService {
|
|
|
196
197
|
processGeometries(payloads, action) {
|
|
197
198
|
const process = action == 'read'
|
|
198
199
|
? (value) => (typeof value === 'string' ? (0, wellknown_1.parse)(value) : value)
|
|
199
|
-
: (value) => this.helpers.st.fromGeoJSON(typeof value == 'string' ?
|
|
200
|
+
: (value) => this.helpers.st.fromGeoJSON(typeof value == 'string' ? (0, parse_json_1.parseJSON)(value) : value);
|
|
200
201
|
const fieldsInCollection = Object.entries(this.schema.collections[this.collection].fields);
|
|
201
202
|
const geometryColumns = fieldsInCollection.filter(([_, field]) => field.type.startsWith('geometry'));
|
|
202
203
|
for (const [name] of geometryColumns) {
|
|
@@ -318,7 +319,7 @@ class PayloadService {
|
|
|
318
319
|
}
|
|
319
320
|
const allowedCollections = relation.meta.one_allowed_collections;
|
|
320
321
|
if (allowedCollections.includes(relatedCollection) === false) {
|
|
321
|
-
throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}`);
|
|
322
|
+
throw new exceptions_1.InvalidPayloadException(`"${relation.collection}.${relation.field}" can't be linked to collection "${relatedCollection}"`);
|
|
322
323
|
}
|
|
323
324
|
const itemsService = new items_1.ItemsService(relatedCollection, {
|
|
324
325
|
accountability: this.accountability,
|
package/dist/services/users.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare class UsersService extends ItemsService {
|
|
|
18
18
|
*/
|
|
19
19
|
private checkPasswordPolicy;
|
|
20
20
|
private checkRemainingAdminExistence;
|
|
21
|
+
/**
|
|
22
|
+
* Make sure there's at least one active admin user when updating user status
|
|
23
|
+
*/
|
|
24
|
+
private checkRemainingActiveAdmin;
|
|
21
25
|
/**
|
|
22
26
|
* Create a new user
|
|
23
27
|
*/
|
package/dist/services/users.js
CHANGED
|
@@ -101,6 +101,23 @@ class UsersService extends items_1.ItemsService {
|
|
|
101
101
|
throw new exceptions_2.UnprocessableEntityException(`You can't remove the last admin user from the role.`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Make sure there's at least one active admin user when updating user status
|
|
106
|
+
*/
|
|
107
|
+
async checkRemainingActiveAdmin(excludeKeys) {
|
|
108
|
+
const otherAdminUsers = await this.knex
|
|
109
|
+
.count('*', { as: 'count' })
|
|
110
|
+
.from('directus_users')
|
|
111
|
+
.whereNotIn('directus_users.id', excludeKeys)
|
|
112
|
+
.andWhere({ 'directus_roles.admin_access': true })
|
|
113
|
+
.andWhere({ 'directus_users.status': 'active' })
|
|
114
|
+
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
|
|
115
|
+
.first();
|
|
116
|
+
const otherAdminUsersCount = +((otherAdminUsers === null || otherAdminUsers === void 0 ? void 0 : otherAdminUsers.count) || 0);
|
|
117
|
+
if (otherAdminUsersCount === 0) {
|
|
118
|
+
throw new exceptions_2.UnprocessableEntityException(`You can't change the active status of the last admin user.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
104
121
|
/**
|
|
105
122
|
* Create a new user
|
|
106
123
|
*/
|
|
@@ -149,6 +166,9 @@ class UsersService extends items_1.ItemsService {
|
|
|
149
166
|
await this.checkRemainingAdminExistence(keys);
|
|
150
167
|
}
|
|
151
168
|
}
|
|
169
|
+
if (data.status !== undefined && data.status !== 'active') {
|
|
170
|
+
await this.checkRemainingActiveAdmin(keys);
|
|
171
|
+
}
|
|
152
172
|
if (data.email) {
|
|
153
173
|
if (keys.length > 1) {
|
|
154
174
|
throw new record_not_unique_1.RecordNotUniqueException('email', {
|
|
@@ -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,14 +3,15 @@ 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
7
|
const lodash_1 = require("lodash");
|
|
8
8
|
const nanoid_1 = require("nanoid");
|
|
9
9
|
const uuid_validate_1 = __importDefault(require("uuid-validate"));
|
|
10
|
-
const exceptions_1 = require("../exceptions");
|
|
11
|
-
const get_column_1 = require("./get-column");
|
|
12
|
-
const get_relation_type_1 = require("./get-relation-type");
|
|
13
10
|
const helpers_1 = require("../database/helpers");
|
|
11
|
+
const invalid_query_1 = require("../exceptions/invalid-query");
|
|
12
|
+
const get_column_1 = require("./get-column");
|
|
13
|
+
const get_column_path_1 = require("./get-column-path");
|
|
14
|
+
const get_relation_info_1 = require("./get-relation-info");
|
|
14
15
|
const utils_1 = require("@directus/shared/utils");
|
|
15
16
|
const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz', 5);
|
|
16
17
|
/**
|
|
@@ -18,18 +19,7 @@ const generateAlias = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz',
|
|
|
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 invalid_query_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 invalid_query_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);
|
|
@@ -311,24 +314,48 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
311
314
|
if (operator === '_neq') {
|
|
312
315
|
dbQuery[logical].whereNot(selectionRaw, compareValue);
|
|
313
316
|
}
|
|
317
|
+
if (operator === '_ieq') {
|
|
318
|
+
dbQuery[logical].whereRaw(`LOWER(??) = ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
|
|
319
|
+
}
|
|
320
|
+
if (operator === '_nieq') {
|
|
321
|
+
dbQuery[logical].whereRaw(`LOWER(??) <> ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
|
|
322
|
+
}
|
|
314
323
|
if (operator === '_contains') {
|
|
315
324
|
dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
|
|
316
325
|
}
|
|
317
326
|
if (operator === '_ncontains') {
|
|
318
327
|
dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
|
|
319
328
|
}
|
|
329
|
+
if (operator === '_icontains') {
|
|
330
|
+
dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
|
|
331
|
+
}
|
|
332
|
+
if (operator === '_nicontains') {
|
|
333
|
+
dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
|
|
334
|
+
}
|
|
320
335
|
if (operator === '_starts_with') {
|
|
321
336
|
dbQuery[logical].where(key, 'like', `${compareValue}%`);
|
|
322
337
|
}
|
|
323
338
|
if (operator === '_nstarts_with') {
|
|
324
339
|
dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
|
|
325
340
|
}
|
|
341
|
+
if (operator === '_istarts_with') {
|
|
342
|
+
dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
|
|
343
|
+
}
|
|
344
|
+
if (operator === '_nistarts_with') {
|
|
345
|
+
dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
|
|
346
|
+
}
|
|
326
347
|
if (operator === '_ends_with') {
|
|
327
348
|
dbQuery[logical].where(key, 'like', `%${compareValue}`);
|
|
328
349
|
}
|
|
329
350
|
if (operator === '_nends_with') {
|
|
330
351
|
dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
|
|
331
352
|
}
|
|
353
|
+
if (operator === '_iends_with') {
|
|
354
|
+
dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
|
|
355
|
+
}
|
|
356
|
+
if (operator === '_niends_with') {
|
|
357
|
+
dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
|
|
358
|
+
}
|
|
332
359
|
if (operator === '_gt') {
|
|
333
360
|
dbQuery[logical].where(selectionRaw, '>', compareValue);
|
|
334
361
|
}
|
|
@@ -382,41 +409,6 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
382
409
|
dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
|
|
383
410
|
}
|
|
384
411
|
}
|
|
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
412
|
}
|
|
421
413
|
}
|
|
422
414
|
exports.applyFilter = applyFilter;
|
|
@@ -450,6 +442,9 @@ function applyAggregate(dbQuery, aggregate, collection) {
|
|
|
450
442
|
if (operation === 'avgDistinct') {
|
|
451
443
|
dbQuery.avgDistinct(`${collection}.${field}`, { as: `avgDistinct->${field}` });
|
|
452
444
|
}
|
|
445
|
+
if (operation === 'countAll') {
|
|
446
|
+
dbQuery.count('*', { as: 'countAll' });
|
|
447
|
+
}
|
|
453
448
|
if (operation === 'count') {
|
|
454
449
|
if (field === '*') {
|
|
455
450
|
dbQuery.count('*', { as: 'count' });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
|
|
2
|
-
import { Knex } from 'knex';
|
|
3
|
-
import { Diff } from 'deep-diff';
|
|
4
1
|
import { SchemaOverview } from '@directus/shared/types';
|
|
2
|
+
import { Diff } from 'deep-diff';
|
|
3
|
+
import { Knex } from 'knex';
|
|
4
|
+
import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
|
|
5
5
|
export declare function applySnapshot(snapshot: Snapshot, options?: {
|
|
6
6
|
database?: Knex;
|
|
7
7
|
schema?: SchemaOverview;
|