directus 9.2.1 → 9.4.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/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 +6 -2
- package/dist/auth/drivers/local.d.ts +2 -2
- package/dist/auth/drivers/local.js +5 -12
- package/dist/auth/drivers/oauth2.d.ts +3 -3
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.d.ts +3 -3
- package/dist/auth/drivers/openid.js +2 -3
- 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 +38 -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/_defaults.yaml +2 -0
- package/dist/database/system-data/fields/sessions.yaml +1 -1
- package/dist/database/system-data/fields/settings.yaml +9 -0
- package/dist/database/system-data/fields/shares.yaml +77 -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/env.js +1 -1
- package/dist/exceptions/index.d.ts +1 -0
- package/dist/exceptions/index.js +1 -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 +20 -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 +66 -67
- package/dist/utils/apply-snapshot.js +69 -14
- package/dist/utils/get-ast-from-query.js +3 -3
- package/dist/utils/get-default-value.js +3 -1
- 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 +27 -19
- 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/example.env +1 -1
- package/package.json +14 -13
package/dist/services/users.js
CHANGED
|
@@ -19,6 +19,7 @@ const mail_1 = require("./mail");
|
|
|
19
19
|
const settings_1 = require("./settings");
|
|
20
20
|
const stall_1 = require("../utils/stall");
|
|
21
21
|
const perf_hooks_1 = require("perf_hooks");
|
|
22
|
+
const utils_2 = require("@directus/shared/utils");
|
|
22
23
|
class UsersService extends items_1.ItemsService {
|
|
23
24
|
constructor(options) {
|
|
24
25
|
super('directus_users', options);
|
|
@@ -251,7 +252,7 @@ class UsersService extends items_1.ItemsService {
|
|
|
251
252
|
}
|
|
252
253
|
const STALL_TIME = 500;
|
|
253
254
|
const timeStart = perf_hooks_1.performance.now();
|
|
254
|
-
const user = await this.knex.select('status').from('directus_users').where({ email }).first();
|
|
255
|
+
const user = await this.knex.select('status', 'password').from('directus_users').where({ email }).first();
|
|
255
256
|
if ((user === null || user === void 0 ? void 0 : user.status) !== 'active') {
|
|
256
257
|
await (0, stall_1.stall)(STALL_TIME, timeStart);
|
|
257
258
|
throw new exceptions_2.ForbiddenException();
|
|
@@ -261,7 +262,7 @@ class UsersService extends items_1.ItemsService {
|
|
|
261
262
|
knex: this.knex,
|
|
262
263
|
accountability: this.accountability,
|
|
263
264
|
});
|
|
264
|
-
const payload = { email, scope: 'password-reset' };
|
|
265
|
+
const payload = { email, scope: 'password-reset', hash: (0, utils_2.getSimpleHash)('' + user.password) };
|
|
265
266
|
const token = jsonwebtoken_1.default.sign(payload, env_1.default.SECRET, { expiresIn: '1d', issuer: 'directus' });
|
|
266
267
|
const acceptURL = url ? `${url}?token=${token}` : `${env_1.default.PUBLIC_URL}/admin/reset-password?token=${token}`;
|
|
267
268
|
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
@@ -279,11 +280,12 @@ class UsersService extends items_1.ItemsService {
|
|
|
279
280
|
await (0, stall_1.stall)(STALL_TIME, timeStart);
|
|
280
281
|
}
|
|
281
282
|
async resetPassword(token, password) {
|
|
282
|
-
const { email, scope } = jsonwebtoken_1.default.verify(token, env_1.default.SECRET, { issuer: 'directus' });
|
|
283
|
-
if (scope !== 'password-reset')
|
|
283
|
+
const { email, scope, hash } = jsonwebtoken_1.default.verify(token, env_1.default.SECRET, { issuer: 'directus' });
|
|
284
|
+
if (scope !== 'password-reset' || !hash)
|
|
284
285
|
throw new exceptions_2.ForbiddenException();
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
await this.checkPasswordPolicy([password]);
|
|
287
|
+
const user = await this.knex.select('id', 'status', 'password').from('directus_users').where({ email }).first();
|
|
288
|
+
if ((user === null || user === void 0 ? void 0 : user.status) !== 'active' || hash !== (0, utils_2.getSimpleHash)('' + user.password)) {
|
|
287
289
|
throw new exceptions_2.ForbiddenException();
|
|
288
290
|
}
|
|
289
291
|
// Allow unauthenticated update
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AbstractServiceOptions, Item, PrimaryKey, Webhook } from '../types';
|
|
2
|
-
import { ItemsService
|
|
1
|
+
import { AbstractServiceOptions, Item, PrimaryKey, Webhook, MutationOptions } from '../types';
|
|
2
|
+
import { ItemsService } from './items';
|
|
3
3
|
export declare class WebhooksService extends ItemsService<Webhook> {
|
|
4
4
|
constructor(options: AbstractServiceOptions);
|
|
5
5
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const knex_1 = __importDefault(require("knex"));
|
|
7
|
+
const knex_mock_client_1 = require("knex-mock-client");
|
|
8
|
+
const run_1 = __importDefault(require("../../../database/migrations/run"));
|
|
9
|
+
describe('run', () => {
|
|
10
|
+
let db;
|
|
11
|
+
let tracker;
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
db = (0, knex_1.default)({ client: knex_mock_client_1.MockClient });
|
|
14
|
+
tracker = (0, knex_mock_client_1.getTracker)();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
tracker.reset();
|
|
18
|
+
});
|
|
19
|
+
describe('when passed the argument up', () => {
|
|
20
|
+
it('returns "Nothing To Updage" if no directus_migrations', async () => {
|
|
21
|
+
// note the difference between an empty array and ['Empty']
|
|
22
|
+
tracker.on.select('directus_migrations').response(['Empty']);
|
|
23
|
+
await (0, run_1.default)(db, 'up').catch((e) => {
|
|
24
|
+
expect(e).toBeInstanceOf(Error);
|
|
25
|
+
expect(e.message).toBe('Nothing to upgrade');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
package/dist/types/ast.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export declare type M2ONode = {
|
|
|
10
10
|
parentKey: string;
|
|
11
11
|
relatedKey: string;
|
|
12
12
|
};
|
|
13
|
-
export declare type
|
|
14
|
-
type: '
|
|
13
|
+
export declare type A2MNode = {
|
|
14
|
+
type: 'a2o';
|
|
15
15
|
names: string[];
|
|
16
16
|
children: {
|
|
17
17
|
[collection: string]: (NestedCollectionNode | FieldNode)[];
|
|
@@ -36,7 +36,7 @@ export declare type O2MNode = {
|
|
|
36
36
|
parentKey: string;
|
|
37
37
|
relatedKey: string;
|
|
38
38
|
};
|
|
39
|
-
export declare type NestedCollectionNode = M2ONode | O2MNode |
|
|
39
|
+
export declare type NestedCollectionNode = M2ONode | O2MNode | A2MNode;
|
|
40
40
|
export declare type FieldNode = {
|
|
41
41
|
type: 'field';
|
|
42
42
|
name: string;
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -15,11 +15,42 @@ export interface User {
|
|
|
15
15
|
provider: string;
|
|
16
16
|
external_identifier: string | null;
|
|
17
17
|
auth_data: string | Record<string, unknown> | null;
|
|
18
|
+
app_access: boolean;
|
|
19
|
+
admin_access: boolean;
|
|
18
20
|
}
|
|
19
21
|
export declare type AuthData = Record<string, any> | null;
|
|
20
22
|
export interface Session {
|
|
21
23
|
token: string;
|
|
22
24
|
expires: Date;
|
|
23
25
|
data: string | Record<string, unknown> | null;
|
|
26
|
+
share: string;
|
|
24
27
|
}
|
|
25
28
|
export declare type SessionData = Record<string, any> | null;
|
|
29
|
+
export declare type DirectusTokenPayload = {
|
|
30
|
+
id?: string;
|
|
31
|
+
role: string | null;
|
|
32
|
+
app_access: boolean | number;
|
|
33
|
+
admin_access: boolean | number;
|
|
34
|
+
share?: string;
|
|
35
|
+
share_scope?: {
|
|
36
|
+
collection: string;
|
|
37
|
+
item: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export declare type ShareData = {
|
|
41
|
+
share_id: string;
|
|
42
|
+
share_role: string;
|
|
43
|
+
share_item: string;
|
|
44
|
+
share_collection: string;
|
|
45
|
+
share_start: Date;
|
|
46
|
+
share_end: Date;
|
|
47
|
+
share_times_used: number;
|
|
48
|
+
share_max_uses?: number;
|
|
49
|
+
share_password?: string;
|
|
50
|
+
};
|
|
51
|
+
export declare type LoginResult = {
|
|
52
|
+
accessToken: any;
|
|
53
|
+
refreshToken: any;
|
|
54
|
+
expires: any;
|
|
55
|
+
id?: any;
|
|
56
|
+
};
|
|
@@ -5,6 +5,7 @@ import { Logger } from 'pino';
|
|
|
5
5
|
import env from '../env';
|
|
6
6
|
import * as exceptions from '../exceptions';
|
|
7
7
|
import * as services from '../services';
|
|
8
|
+
import { Emitter } from '../emitter';
|
|
8
9
|
import { getSchema } from '../utils/get-schema';
|
|
9
10
|
import { SchemaOverview } from './schema';
|
|
10
11
|
export declare type ExtensionContext = {
|
|
@@ -12,6 +13,7 @@ export declare type ExtensionContext = {
|
|
|
12
13
|
exceptions: typeof exceptions;
|
|
13
14
|
database: Knex;
|
|
14
15
|
env: typeof env;
|
|
16
|
+
emitter: Emitter;
|
|
15
17
|
logger: Logger;
|
|
16
18
|
getSchema: typeof getSchema;
|
|
17
19
|
};
|
package/dist/types/items.d.ts
CHANGED
|
@@ -13,3 +13,17 @@ export declare type Alterations = {
|
|
|
13
13
|
}[];
|
|
14
14
|
delete: (number | string)[];
|
|
15
15
|
};
|
|
16
|
+
export declare type MutationOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Callback function that's fired whenever a revision is made in the mutation
|
|
19
|
+
*/
|
|
20
|
+
onRevisionCreate?: (pk: PrimaryKey) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Flag to disable the auto purging of the cache. Is ignored when CACHE_AUTO_PURGE isn't enabled.
|
|
23
|
+
*/
|
|
24
|
+
autoPurgeCache?: false;
|
|
25
|
+
/**
|
|
26
|
+
* Allow disabling the emitting of hooks. Useful if a custom hook is fired (like files.upload)
|
|
27
|
+
*/
|
|
28
|
+
emitEvents?: boolean;
|
|
29
|
+
};
|
|
@@ -5,44 +5,6 @@ import { Aggregate, Filter, Query } from '@directus/shared/types';
|
|
|
5
5
|
* Apply the Query to a given Knex query builder instance
|
|
6
6
|
*/
|
|
7
7
|
export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, subQuery?: boolean): Knex.QueryBuilder;
|
|
8
|
-
/**
|
|
9
|
-
* Apply a given filter object to the Knex QueryBuilder instance.
|
|
10
|
-
*
|
|
11
|
-
* Relational nested filters, like the following example:
|
|
12
|
-
*
|
|
13
|
-
* ```json
|
|
14
|
-
* // Fetch pages that have articles written by Rijk
|
|
15
|
-
*
|
|
16
|
-
* {
|
|
17
|
-
* "articles": {
|
|
18
|
-
* "author": {
|
|
19
|
-
* "name": {
|
|
20
|
-
* "_eq": "Rijk"
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* are handled by joining the nested tables, and using a where statement on the top level on the
|
|
28
|
-
* nested field through the join. This allows us to filter the top level items based on nested data.
|
|
29
|
-
* The where on the root is done with a subquery to prevent duplicates, any nested joins are done
|
|
30
|
-
* with aliases to prevent naming conflicts.
|
|
31
|
-
*
|
|
32
|
-
* The output SQL for the above would look something like:
|
|
33
|
-
*
|
|
34
|
-
* ```sql
|
|
35
|
-
* SELECT *
|
|
36
|
-
* FROM pages
|
|
37
|
-
* WHERE
|
|
38
|
-
* pages.id in (
|
|
39
|
-
* SELECT articles.page_id AS page_id
|
|
40
|
-
* FROM articles
|
|
41
|
-
* LEFT JOIN authors AS xviqp ON articles.author = xviqp.id
|
|
42
|
-
* WHERE xviqp.name = 'Rijk'
|
|
43
|
-
* )
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
8
|
export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, subQuery?: boolean): Knex.QueryBuilder<any, any>;
|
|
47
9
|
export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
|
|
48
10
|
export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
@@ -76,44 +76,44 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
|
|
|
76
76
|
return dbQuery;
|
|
77
77
|
}
|
|
78
78
|
exports.default = applyQuery;
|
|
79
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
117
117
|
function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
|
|
118
118
|
const helpers = (0, helpers_1.getHelpers)(knex);
|
|
119
119
|
const relations = schema.relations;
|
|
@@ -143,31 +143,34 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
143
143
|
followRelation(path);
|
|
144
144
|
function followRelation(pathParts, parentCollection = collection, parentAlias) {
|
|
145
145
|
/**
|
|
146
|
-
* For
|
|
146
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
147
147
|
*/
|
|
148
148
|
const pathRoot = pathParts[0].split(':')[0];
|
|
149
|
-
const relation = relations
|
|
150
|
-
|
|
151
|
-
return ((relation.collection === parentCollection && relation.field === pathRoot) ||
|
|
152
|
-
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
153
|
-
});
|
|
154
|
-
if (!relation)
|
|
149
|
+
const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
|
|
150
|
+
if (!relation) {
|
|
155
151
|
return;
|
|
156
|
-
|
|
152
|
+
}
|
|
157
153
|
const alias = generateAlias();
|
|
158
154
|
(0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
|
|
159
155
|
if (relationType === 'm2o') {
|
|
160
156
|
dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
|
|
161
157
|
}
|
|
162
|
-
if (relationType === '
|
|
158
|
+
if (relationType === 'a2o') {
|
|
163
159
|
const pathScope = pathParts[0].split(':')[1];
|
|
164
160
|
if (!pathScope) {
|
|
165
161
|
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
166
162
|
}
|
|
167
163
|
dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
|
|
168
164
|
joinClause
|
|
169
|
-
.
|
|
170
|
-
.
|
|
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}`));
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
// Still join o2m relations when in subquery OR when the o2m relation is not at the root level
|
|
@@ -179,7 +182,7 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
179
182
|
if (relationType === 'm2o') {
|
|
180
183
|
parent = relation.related_collection;
|
|
181
184
|
}
|
|
182
|
-
else if (relationType === '
|
|
185
|
+
else if (relationType === 'a2o') {
|
|
183
186
|
const pathScope = pathParts[0].split(':')[1];
|
|
184
187
|
if (!pathScope) {
|
|
185
188
|
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
@@ -215,17 +218,12 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
215
218
|
}
|
|
216
219
|
const filterPath = getFilterPath(key, value);
|
|
217
220
|
/**
|
|
218
|
-
* For
|
|
221
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
219
222
|
*/
|
|
220
223
|
const pathRoot = filterPath[0].split(':')[0];
|
|
221
|
-
const relation = relations
|
|
222
|
-
var _a;
|
|
223
|
-
return ((relation.collection === collection && relation.field === pathRoot) ||
|
|
224
|
-
(relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
225
|
-
});
|
|
224
|
+
const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
|
|
226
225
|
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
|
227
|
-
|
|
228
|
-
if (relationType === 'm2o' || relationType === 'm2a' || relationType === null) {
|
|
226
|
+
if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
|
|
229
227
|
if (filterPath.length > 1) {
|
|
230
228
|
const columnName = getWhereColumn(filterPath, collection);
|
|
231
229
|
if (!columnName)
|
|
@@ -237,7 +235,13 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
else if (subQuery === false) {
|
|
240
|
-
|
|
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
|
|
241
245
|
dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
|
|
242
246
|
const field = relation.field;
|
|
243
247
|
const collection = relation.collection;
|
|
@@ -376,22 +380,17 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
376
380
|
return followRelation(path);
|
|
377
381
|
function followRelation(pathParts, parentCollection = collection, parentAlias) {
|
|
378
382
|
/**
|
|
379
|
-
* For
|
|
383
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
380
384
|
*/
|
|
381
385
|
const pathRoot = pathParts[0].split(':')[0];
|
|
382
|
-
const relation = relations
|
|
383
|
-
var _a;
|
|
384
|
-
return ((relation.collection === parentCollection && relation.field === pathRoot) ||
|
|
385
|
-
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
386
|
-
});
|
|
386
|
+
const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
|
|
387
387
|
if (!relation) {
|
|
388
388
|
throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
|
|
389
389
|
}
|
|
390
|
-
const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
|
|
391
390
|
const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
|
|
392
391
|
const remainingParts = pathParts.slice(1);
|
|
393
392
|
let parent;
|
|
394
|
-
if (relationType === '
|
|
393
|
+
if (relationType === 'a2o') {
|
|
395
394
|
const pathScope = pathParts[0].split(':')[1];
|
|
396
395
|
if (!pathScope) {
|
|
397
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: {},
|
|
@@ -16,12 +16,14 @@ function getDefaultValue(column) {
|
|
|
16
16
|
return null;
|
|
17
17
|
if (defaultValue === 'NULL')
|
|
18
18
|
return null;
|
|
19
|
-
// Check if the default is wrapped in an extra pair of quotes, this happens in SQLite
|
|
19
|
+
// Check if the default is wrapped in an extra pair of quotes, this happens in SQLite / MariaDB
|
|
20
20
|
if (typeof defaultValue === 'string' &&
|
|
21
21
|
((defaultValue.startsWith(`'`) && defaultValue.endsWith(`'`)) ||
|
|
22
22
|
(defaultValue.startsWith(`"`) && defaultValue.endsWith(`"`)))) {
|
|
23
23
|
defaultValue = defaultValue.slice(1, -1);
|
|
24
24
|
}
|
|
25
|
+
if (defaultValue === '0000-00-00 00:00:00')
|
|
26
|
+
return null;
|
|
25
27
|
switch (type) {
|
|
26
28
|
case 'bigInteger':
|
|
27
29
|
case 'integer':
|
|
@@ -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[]>;
|