directus 9.8.0 → 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/__mocks__/cache.d.ts +5 -0
- package/dist/__mocks__/cache.js +7 -0
- package/dist/app.js +3 -0
- package/dist/auth/drivers/ldap.js +10 -11
- package/dist/auth/drivers/oauth2.js +11 -6
- package/dist/auth/drivers/openid.js +9 -6
- package/dist/cli/commands/schema/apply.js +9 -3
- package/dist/controllers/assets.js +5 -0
- package/dist/controllers/files.d.ts +2 -0
- package/dist/controllers/files.js +13 -5
- package/dist/database/helpers/date/dialects/mssql.d.ts +4 -0
- package/dist/database/helpers/date/dialects/mssql.js +12 -0
- package/dist/database/helpers/date/dialects/mysql.d.ts +5 -0
- package/dist/database/helpers/date/dialects/mysql.js +16 -0
- package/dist/database/helpers/date/dialects/oracle.d.ts +4 -0
- package/dist/database/helpers/date/dialects/oracle.js +15 -0
- package/dist/database/helpers/date/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/date/dialects/sqlite.js +8 -0
- package/dist/database/helpers/date/index.d.ts +3 -3
- package/dist/database/helpers/date/index.js +6 -6
- package/dist/database/helpers/date/types.d.ts +3 -0
- package/dist/database/helpers/date/types.js +10 -0
- package/dist/database/helpers/fn/dialects/postgres.js +5 -1
- package/dist/database/helpers/index.d.ts +1 -1
- package/dist/database/migrations/20220402A-remove-default-value-panel-icon.d.ts +3 -0
- package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +22 -0
- package/dist/database/run-ast.js +3 -3
- package/dist/database/system-data/fields/collections.yaml +1 -1
- package/dist/database/system-data/fields/settings.yaml +0 -1
- package/dist/database/system-data/fields/users.yaml +3 -0
- package/dist/env.js +9 -3
- package/dist/exceptions/index.d.ts +1 -0
- package/dist/exceptions/index.js +1 -0
- package/dist/exceptions/token-expired.d.ts +4 -0
- package/dist/exceptions/token-expired.js +10 -0
- package/dist/middleware/cache.js +10 -0
- package/dist/services/authorization.js +72 -30
- package/dist/services/collections.d.ts +2 -0
- package/dist/services/collections.js +10 -0
- package/dist/services/fields.js +26 -1
- package/dist/services/files.d.ts +5 -1
- package/dist/services/files.js +59 -40
- package/dist/services/graphql.d.ts +2 -3
- package/dist/services/graphql.js +65 -11
- package/dist/services/import-export.js +2 -0
- package/dist/services/items.js +12 -5
- package/dist/services/payload.d.ts +2 -1
- package/dist/services/payload.js +22 -17
- package/dist/services/specifications.js +1 -3
- package/dist/services/users.js +4 -1
- package/dist/types/files.d.ts +8 -0
- package/dist/utils/apply-query.d.ts +2 -1
- package/dist/utils/apply-query.js +134 -156
- package/dist/utils/apply-snapshot.d.ts +3 -1
- package/dist/utils/apply-snapshot.js +34 -5
- package/dist/utils/get-ast-from-query.js +15 -3
- package/dist/utils/get-column-path.d.ts +16 -0
- package/dist/utils/get-column-path.js +46 -0
- package/dist/utils/get-graphql-type.js +1 -0
- package/dist/utils/get-local-type.js +5 -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/get-schema.js +3 -0
- package/dist/utils/jwt.js +1 -1
- package/dist/utils/merge-permissions-for-share.js +1 -1
- package/dist/utils/reduce-schema.js +18 -11
- package/dist/utils/validate-query.js +19 -15
- package/example.env +4 -0
- package/package.json +18 -19
|
@@ -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,72 +169,21 @@ 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
|
}
|
|
182
185
|
function addWhereClauses(knex, dbQuery, filter, collection, logical = 'and') {
|
|
186
|
+
var _a, _b;
|
|
183
187
|
for (const [key, value] of Object.entries(filter)) {
|
|
184
188
|
if (key === '_or' || key === '_and') {
|
|
185
189
|
// If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
|
|
@@ -200,11 +204,11 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
200
204
|
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
201
205
|
*/
|
|
202
206
|
const pathRoot = filterPath[0].split(':')[0];
|
|
203
|
-
const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
|
|
207
|
+
const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, collection, pathRoot);
|
|
204
208
|
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
|
205
209
|
if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
|
|
206
210
|
if (filterPath.length > 1) {
|
|
207
|
-
const columnName =
|
|
211
|
+
const columnName = (0, get_column_path_1.getColumnPath)({ path: filterPath, collection, relations, aliasMap });
|
|
208
212
|
if (!columnName)
|
|
209
213
|
continue;
|
|
210
214
|
applyFilterToQuery(columnName, filterOperator, filterValue, logical);
|
|
@@ -220,16 +224,25 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
220
224
|
if (relationType === 'o2a') {
|
|
221
225
|
pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
|
|
222
226
|
}
|
|
223
|
-
|
|
224
|
-
dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
|
|
227
|
+
const subQueryBuilder = (filter) => (subQueryKnex) => {
|
|
225
228
|
const field = relation.field;
|
|
226
229
|
const collection = relation.collection;
|
|
227
230
|
const column = `${collection}.${field}`;
|
|
228
|
-
subQueryKnex
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
subQueryKnex
|
|
232
|
+
.select({ [field]: column })
|
|
233
|
+
.from(collection)
|
|
234
|
+
.whereNotNull(column);
|
|
235
|
+
applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, true);
|
|
236
|
+
};
|
|
237
|
+
if (((_a = Object.keys(value)) === null || _a === void 0 ? void 0 : _a[0]) === '_none') {
|
|
238
|
+
dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0]));
|
|
239
|
+
}
|
|
240
|
+
else if (((_b = Object.keys(value)) === null || _b === void 0 ? void 0 : _b[0]) === '_some') {
|
|
241
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0]));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(value));
|
|
245
|
+
}
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
function applyFilterToQuery(key, operator, compareValue, logical = 'and') {
|
|
@@ -372,41 +385,6 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
372
385
|
dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
|
-
function getWhereColumn(path, collection) {
|
|
376
|
-
return followRelation(path);
|
|
377
|
-
function followRelation(pathParts, parentCollection = collection, parentAlias) {
|
|
378
|
-
/**
|
|
379
|
-
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
380
|
-
*/
|
|
381
|
-
const pathRoot = pathParts[0].split(':')[0];
|
|
382
|
-
const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
|
|
383
|
-
if (!relation) {
|
|
384
|
-
throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
|
|
385
|
-
}
|
|
386
|
-
const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
|
|
387
|
-
const remainingParts = pathParts.slice(1);
|
|
388
|
-
let parent;
|
|
389
|
-
if (relationType === 'a2o') {
|
|
390
|
-
const pathScope = pathParts[0].split(':')[1];
|
|
391
|
-
if (!pathScope) {
|
|
392
|
-
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
393
|
-
}
|
|
394
|
-
parent = pathScope;
|
|
395
|
-
}
|
|
396
|
-
else if (relationType === 'm2o') {
|
|
397
|
-
parent = relation.related_collection;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
parent = relation.collection;
|
|
401
|
-
}
|
|
402
|
-
if (remainingParts.length === 1) {
|
|
403
|
-
return `${alias || parent}.${remainingParts[0]}`;
|
|
404
|
-
}
|
|
405
|
-
if (remainingParts.length) {
|
|
406
|
-
return followRelation(remainingParts, parent, alias);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
388
|
}
|
|
411
389
|
}
|
|
412
390
|
exports.applyFilter = applyFilter;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Snapshot, SnapshotDiff } from '../types';
|
|
1
|
+
import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
|
|
2
2
|
import { Knex } from 'knex';
|
|
3
|
+
import { Diff } from 'deep-diff';
|
|
3
4
|
import { SchemaOverview } from '@directus/shared/types';
|
|
4
5
|
export declare function applySnapshot(snapshot: Snapshot, options?: {
|
|
5
6
|
database?: Knex;
|
|
@@ -7,3 +8,4 @@ export declare function applySnapshot(snapshot: Snapshot, options?: {
|
|
|
7
8
|
current?: Snapshot;
|
|
8
9
|
diff?: SnapshotDiff;
|
|
9
10
|
}): Promise<void>;
|
|
11
|
+
export declare function isNestedMetaUpdate(diff: Diff<SnapshotField | undefined>): boolean;
|
|
@@ -3,7 +3,7 @@ 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.applySnapshot = void 0;
|
|
6
|
+
exports.isNestedMetaUpdate = exports.applySnapshot = void 0;
|
|
7
7
|
const get_snapshot_1 = require("./get-snapshot");
|
|
8
8
|
const get_snapshot_diff_1 = require("./get-snapshot-diff");
|
|
9
9
|
const database_1 = __importDefault(require("../database"));
|
|
@@ -34,7 +34,26 @@ async function applySnapshot(snapshot, options) {
|
|
|
34
34
|
// creating a collection without a primary key
|
|
35
35
|
const fields = snapshotDiff.fields
|
|
36
36
|
.filter((fieldDiff) => fieldDiff.collection === collection)
|
|
37
|
-
.map((fieldDiff) => fieldDiff.diff[0].rhs)
|
|
37
|
+
.map((fieldDiff) => fieldDiff.diff[0].rhs)
|
|
38
|
+
.map((fieldDiff) => {
|
|
39
|
+
// Casts field type to UUID when applying SQLite-based schema on other databases.
|
|
40
|
+
// This is needed because SQLite snapshots UUID fields as char with length 36, and
|
|
41
|
+
// it will fail when trying to create relation between char field to UUID field
|
|
42
|
+
if (!fieldDiff.schema ||
|
|
43
|
+
fieldDiff.schema.data_type !== 'char' ||
|
|
44
|
+
fieldDiff.schema.max_length !== 36 ||
|
|
45
|
+
!fieldDiff.schema.foreign_key_table ||
|
|
46
|
+
!fieldDiff.schema.foreign_key_column) {
|
|
47
|
+
return fieldDiff;
|
|
48
|
+
}
|
|
49
|
+
const matchingForeignKeyTable = schema.collections[fieldDiff.schema.foreign_key_table];
|
|
50
|
+
if (!matchingForeignKeyTable)
|
|
51
|
+
return fieldDiff;
|
|
52
|
+
const matchingForeignKeyField = matchingForeignKeyTable.fields[fieldDiff.schema.foreign_key_column];
|
|
53
|
+
if (!matchingForeignKeyField || matchingForeignKeyField.type !== 'uuid')
|
|
54
|
+
return fieldDiff;
|
|
55
|
+
return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
|
|
56
|
+
});
|
|
38
57
|
try {
|
|
39
58
|
await collectionsService.createOne({
|
|
40
59
|
...diff[0].rhs,
|
|
@@ -66,7 +85,7 @@ async function applySnapshot(snapshot, options) {
|
|
|
66
85
|
}
|
|
67
86
|
const fieldsService = new services_1.FieldsService({ knex: trx, schema: await (0, get_schema_1.getSchema)({ database: trx }) });
|
|
68
87
|
for (const { collection, field, diff } of snapshotDiff.fields) {
|
|
69
|
-
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N') {
|
|
88
|
+
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
|
|
70
89
|
try {
|
|
71
90
|
await fieldsService.createField(collection, diff[0].rhs);
|
|
72
91
|
}
|
|
@@ -75,7 +94,7 @@ async function applySnapshot(snapshot, options) {
|
|
|
75
94
|
throw err;
|
|
76
95
|
}
|
|
77
96
|
}
|
|
78
|
-
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
|
|
97
|
+
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A' || isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
|
|
79
98
|
const newValues = snapshot.fields.find((snapshotField) => {
|
|
80
99
|
return snapshotField.collection === collection && snapshotField.field === field;
|
|
81
100
|
});
|
|
@@ -91,7 +110,7 @@ async function applySnapshot(snapshot, options) {
|
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
|
-
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
|
|
113
|
+
if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
|
|
95
114
|
try {
|
|
96
115
|
await fieldsService.deleteField(collection, field);
|
|
97
116
|
}
|
|
@@ -146,3 +165,13 @@ async function applySnapshot(snapshot, options) {
|
|
|
146
165
|
});
|
|
147
166
|
}
|
|
148
167
|
exports.applySnapshot = applySnapshot;
|
|
168
|
+
function isNestedMetaUpdate(diff) {
|
|
169
|
+
if (!diff)
|
|
170
|
+
return false;
|
|
171
|
+
if (diff.kind !== 'N' && diff.kind !== 'D')
|
|
172
|
+
return false;
|
|
173
|
+
if (!diff.path || diff.path.length < 2 || diff.path[0] !== 'meta')
|
|
174
|
+
return false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
exports.isNestedMetaUpdate = isNestedMetaUpdate;
|
|
@@ -75,9 +75,17 @@ async function getASTFromQuery(collection, query, schema, options) {
|
|
|
75
75
|
const relationalStructure = {};
|
|
76
76
|
for (const fieldKey of fields) {
|
|
77
77
|
let name = fieldKey;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
name
|
|
78
|
+
if (query.alias) {
|
|
79
|
+
// check for field alias (is is one of the key)
|
|
80
|
+
if (name in query.alias) {
|
|
81
|
+
name = query.alias[fieldKey];
|
|
82
|
+
}
|
|
83
|
+
// check for junction alias (it is one of the value instead of the key)
|
|
84
|
+
if (Object.values(query.alias).includes(name)) {
|
|
85
|
+
const aliasKey = Object.keys(query.alias).find((key) => { var _a; return ((_a = query.alias) === null || _a === void 0 ? void 0 : _a[key]) === name; });
|
|
86
|
+
if (aliasKey && fieldKey !== aliasKey)
|
|
87
|
+
name = aliasKey;
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
const isRelational = name.includes('.') ||
|
|
83
91
|
// We'll always treat top level o2m fields as a related item. This is an alias field, otherwise it won't return
|
|
@@ -162,6 +170,10 @@ async function getASTFromQuery(collection, query, schema, options) {
|
|
|
162
170
|
if (permissions && permissions.some((permission) => permission.collection === relatedCollection) === false) {
|
|
163
171
|
continue;
|
|
164
172
|
}
|
|
173
|
+
// update query alias for children parseFields
|
|
174
|
+
const deepAlias = (_a = getDeepQuery((deep === null || deep === void 0 ? void 0 : deep[fieldKey]) || {})) === null || _a === void 0 ? void 0 : _a.alias;
|
|
175
|
+
if (!(0, lodash_1.isEmpty)(deepAlias))
|
|
176
|
+
query.alias = deepAlias;
|
|
165
177
|
child = {
|
|
166
178
|
type: relationType,
|
|
167
179
|
name: relatedCollection,
|
|
@@ -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;
|
|
@@ -88,6 +88,7 @@ const localTypeMap = {
|
|
|
88
88
|
'time without time zone': 'time',
|
|
89
89
|
float4: 'float',
|
|
90
90
|
float8: 'float',
|
|
91
|
+
citext: 'text',
|
|
91
92
|
// Oracle
|
|
92
93
|
number: 'integer',
|
|
93
94
|
sdo_geometry: 'geometry',
|
|
@@ -109,6 +110,10 @@ function getLocalType(column, field) {
|
|
|
109
110
|
return 'csv';
|
|
110
111
|
if (special.includes('uuid') || special.includes('file'))
|
|
111
112
|
return 'uuid';
|
|
113
|
+
if (special.includes('cast-timestamp'))
|
|
114
|
+
return 'timestamp';
|
|
115
|
+
if (special.includes('cast-datetime'))
|
|
116
|
+
return 'dateTime';
|
|
112
117
|
if (type === null || type === void 0 ? void 0 : type.startsWith('geometry')) {
|
|
113
118
|
return special[0] || 'geometry';
|
|
114
119
|
}
|
|
@@ -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;
|
package/dist/utils/get-schema.js
CHANGED
|
@@ -8,6 +8,7 @@ const schema_1 = __importDefault(require("@directus/schema"));
|
|
|
8
8
|
const utils_1 = require("@directus/shared/utils");
|
|
9
9
|
const lodash_1 = require("lodash");
|
|
10
10
|
const cache_1 = require("../cache");
|
|
11
|
+
const constants_1 = require("../constants");
|
|
11
12
|
const database_1 = __importDefault(require("../database"));
|
|
12
13
|
const collections_1 = require("../database/system-data/collections");
|
|
13
14
|
const fields_1 = require("../database/system-data/fields");
|
|
@@ -113,6 +114,8 @@ async function getDatabaseSchema(database, schemaInspector) {
|
|
|
113
114
|
const existing = result.collections[field.collection].fields[field.field];
|
|
114
115
|
const column = schemaOverview[field.collection].columns[field.field];
|
|
115
116
|
const special = field.special ? (0, utils_1.toArray)(field.special) : [];
|
|
117
|
+
if (constants_1.ALIAS_TYPES.some((type) => special.includes(type)) === false && !existing)
|
|
118
|
+
continue;
|
|
116
119
|
const type = (existing && (0, get_local_type_1.default)(column, { special })) || 'alias';
|
|
117
120
|
let validation = (_a = field.validation) !== null && _a !== void 0 ? _a : null;
|
|
118
121
|
if (validation && typeof validation === 'string')
|