@directus/api 22.0.0 → 22.1.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/cli/commands/init/questions.d.ts +7 -6
- package/dist/cli/commands/init/questions.js +2 -2
- package/dist/cli/utils/create-env/index.d.ts +2 -2
- package/dist/cli/utils/create-env/index.js +3 -1
- package/dist/cli/utils/drivers.js +1 -1
- package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
- package/dist/database/helpers/fn/types.d.ts +6 -3
- package/dist/database/helpers/fn/types.js +2 -2
- package/dist/database/helpers/index.d.ts +3 -3
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
- package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
- package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
- package/dist/database/migrations/20240806A-permissions-policies.js +18 -3
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +9 -5
- package/dist/database/run-ast/run-ast.d.ts +2 -2
- package/dist/database/run-ast/run-ast.js +14 -7
- package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
- package/dist/database/run-ast/utils/apply-case-when.js +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
- package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
- package/dist/permissions/lib/fetch-permissions.js +5 -39
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
- package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
- package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
- package/dist/permissions/modules/process-payload/process-payload.js +4 -5
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
- package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
- package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
- package/dist/server.js +17 -4
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +22 -19
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/meta.js +8 -7
- package/dist/services/permissions.js +19 -19
- package/dist/types/database.d.ts +1 -1
- package/dist/utils/apply-query.d.ts +3 -3
- package/dist/utils/apply-query.js +25 -20
- package/dist/utils/get-address.d.ts +5 -0
- package/dist/utils/get-address.js +13 -0
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/transaction.js +28 -11
- package/dist/websocket/controllers/graphql.js +2 -3
- package/dist/websocket/controllers/rest.js +2 -3
- package/package.json +17 -16
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
import type { Driver } from '../../../types/index.js';
|
|
1
2
|
export declare const databaseQuestions: {
|
|
2
3
|
sqlite3: (({ filepath }: {
|
|
3
4
|
filepath: string;
|
|
4
5
|
}) => Record<string, string>)[];
|
|
5
|
-
|
|
6
|
-
client:
|
|
6
|
+
mysql2: (({ client }: {
|
|
7
|
+
client: Exclude<Driver, 'sqlite3'>;
|
|
7
8
|
}) => Record<string, any>)[];
|
|
8
9
|
pg: (({ client }: {
|
|
9
|
-
client:
|
|
10
|
+
client: Exclude<Driver, 'sqlite3'>;
|
|
10
11
|
}) => Record<string, any>)[];
|
|
11
12
|
cockroachdb: (({ client }: {
|
|
12
|
-
client:
|
|
13
|
+
client: Exclude<Driver, 'sqlite3'>;
|
|
13
14
|
}) => Record<string, any>)[];
|
|
14
15
|
oracledb: (({ client }: {
|
|
15
|
-
client:
|
|
16
|
+
client: Exclude<Driver, 'sqlite3'>;
|
|
16
17
|
}) => Record<string, any>)[];
|
|
17
18
|
mssql: (({ client }: {
|
|
18
|
-
client:
|
|
19
|
+
client: Exclude<Driver, 'sqlite3'>;
|
|
19
20
|
}) => Record<string, any>)[];
|
|
20
21
|
};
|
|
@@ -19,7 +19,7 @@ const port = ({ client }) => ({
|
|
|
19
19
|
const ports = {
|
|
20
20
|
pg: 5432,
|
|
21
21
|
cockroachdb: 26257,
|
|
22
|
-
|
|
22
|
+
mysql2: 3306,
|
|
23
23
|
oracledb: 1521,
|
|
24
24
|
mssql: 1433,
|
|
25
25
|
};
|
|
@@ -57,7 +57,7 @@ const ssl = () => ({
|
|
|
57
57
|
});
|
|
58
58
|
export const databaseQuestions = {
|
|
59
59
|
sqlite3: [filename],
|
|
60
|
-
|
|
60
|
+
mysql2: [host, port, database, user, password],
|
|
61
61
|
pg: [host, port, database, user, password, ssl],
|
|
62
62
|
cockroachdb: [host, port, database, user, password, ssl],
|
|
63
63
|
oracledb: [host, port, database, user, password],
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
+
import type { Driver } from '../../../types/index.js';
|
|
1
2
|
import type { Credentials } from '../create-db-connection.js';
|
|
2
|
-
|
|
3
|
-
export default function createEnv(client: keyof typeof drivers, credentials: Credentials, directory: string): Promise<void>;
|
|
3
|
+
export default function createEnv(client: Driver, credentials: Credentials, directory: string): Promise<void>;
|
|
@@ -14,12 +14,14 @@ const liquidEngine = new Liquid({
|
|
|
14
14
|
});
|
|
15
15
|
export default async function createEnv(client, credentials, directory) {
|
|
16
16
|
const { nanoid } = await import('nanoid');
|
|
17
|
+
// For backwards-compatibility, DB_CLIENT is still 'mysql'
|
|
18
|
+
const dbClient = client === 'mysql2' ? 'mysql' : client;
|
|
17
19
|
const config = {
|
|
18
20
|
security: {
|
|
19
21
|
SECRET: nanoid(32),
|
|
20
22
|
},
|
|
21
23
|
database: {
|
|
22
|
-
DB_CLIENT:
|
|
24
|
+
DB_CLIENT: dbClient,
|
|
23
25
|
},
|
|
24
26
|
};
|
|
25
27
|
for (const [key, value] of Object.entries(credentials)) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Generate an AST based on a given collection and query
|
|
3
3
|
*/
|
|
4
4
|
import { cloneDeep, uniq } from 'lodash-es';
|
|
5
|
-
import { fetchAllowedFields } from '../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
6
5
|
import { parseFields } from './lib/parse-fields.js';
|
|
6
|
+
import { getAllowedSort } from './utils/get-allowed-sort.js';
|
|
7
7
|
export async function getAstFromQuery(options, context) {
|
|
8
8
|
options.query = cloneDeep(options.query);
|
|
9
9
|
const ast = {
|
|
@@ -36,36 +36,7 @@ export async function getAstFromQuery(options, context) {
|
|
|
36
36
|
// Prevent fields/deep from showing up in the query object in further use
|
|
37
37
|
delete options.query.fields;
|
|
38
38
|
delete options.query.deep;
|
|
39
|
-
|
|
40
|
-
// We'll default to the primary key for the standard sort output
|
|
41
|
-
let sortField = context.schema.collections[options.collection].primary;
|
|
42
|
-
// If a custom manual sort field is configured, use that
|
|
43
|
-
if (context.schema.collections[options.collection]?.sortField) {
|
|
44
|
-
sortField = context.schema.collections[options.collection].sortField;
|
|
45
|
-
}
|
|
46
|
-
if (options.accountability && options.accountability.admin === false) {
|
|
47
|
-
// Verify that the user has access to the sort field
|
|
48
|
-
const allowedFields = await fetchAllowedFields({
|
|
49
|
-
collection: options.collection,
|
|
50
|
-
action: 'read',
|
|
51
|
-
accountability: options.accountability,
|
|
52
|
-
}, context);
|
|
53
|
-
if (allowedFields.length === 0) {
|
|
54
|
-
sortField = null;
|
|
55
|
-
}
|
|
56
|
-
else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
|
|
57
|
-
// If the sort field is not allowed, default to the first allowed field
|
|
58
|
-
sortField = allowedFields[0];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// When group by is used, default to the first column provided in the group by clause
|
|
62
|
-
if (options.query.group?.[0]) {
|
|
63
|
-
sortField = options.query.group[0];
|
|
64
|
-
}
|
|
65
|
-
if (sortField) {
|
|
66
|
-
options.query.sort = [sortField];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
39
|
+
options.query.sort ??= await getAllowedSort(options, context);
|
|
69
40
|
// When no group by is supplied, but an aggregate function is used, only a single row will be
|
|
70
41
|
// returned. In those cases, we'll ignore the sort field altogether
|
|
71
42
|
if (options.query.aggregate && Object.keys(options.query.aggregate).length && !options.query.group?.[0]) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
|
-
import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../../../types/index.js';
|
|
3
|
+
import type { FieldNode, FunctionFieldNode, NestedCollectionNode, O2MNode } from '../../../types/index.js';
|
|
4
4
|
export interface ParseFieldsOptions {
|
|
5
5
|
accountability: Accountability | null;
|
|
6
6
|
parentCollection: string;
|
|
@@ -13,3 +13,4 @@ export interface ParseFieldsContext {
|
|
|
13
13
|
knex: Knex;
|
|
14
14
|
}
|
|
15
15
|
export declare function parseFields(options: ParseFieldsOptions, context: ParseFieldsContext): Promise<[] | (NestedCollectionNode | FieldNode | FunctionFieldNode)[]>;
|
|
16
|
+
export declare function isO2MNode(node: NestedCollectionNode | null): node is O2MNode;
|
|
@@ -3,6 +3,7 @@ import { isEmpty } from 'lodash-es';
|
|
|
3
3
|
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
4
4
|
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
5
5
|
import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
6
|
+
import { getAllowedSort } from '../utils/get-allowed-sort.js';
|
|
6
7
|
import { getDeepQuery } from '../utils/get-deep-query.js';
|
|
7
8
|
import { getRelatedCollection } from '../utils/get-related-collection.js';
|
|
8
9
|
import { getRelation } from '../utils/get-relation.js';
|
|
@@ -119,7 +120,16 @@ export async function parseFields(options, context) {
|
|
|
119
120
|
continue;
|
|
120
121
|
let child = null;
|
|
121
122
|
if (relationType === 'a2o') {
|
|
122
|
-
|
|
123
|
+
let allowedCollections = relation.meta.one_allowed_collections;
|
|
124
|
+
if (options.accountability && options.accountability.admin === false && policies) {
|
|
125
|
+
const permissions = await fetchPermissions({
|
|
126
|
+
action: 'read',
|
|
127
|
+
collections: allowedCollections,
|
|
128
|
+
policies: policies,
|
|
129
|
+
accountability: options.accountability,
|
|
130
|
+
}, context);
|
|
131
|
+
allowedCollections = allowedCollections.filter((collection) => permissions.some((permission) => permission.collection === collection));
|
|
132
|
+
}
|
|
123
133
|
child = {
|
|
124
134
|
type: 'a2o',
|
|
125
135
|
names: allowedCollections,
|
|
@@ -181,8 +191,13 @@ export async function parseFields(options, context) {
|
|
|
181
191
|
cases: [],
|
|
182
192
|
whenCase: [],
|
|
183
193
|
};
|
|
184
|
-
if (
|
|
185
|
-
child.query.sort =
|
|
194
|
+
if (isO2MNode(child) && !child.query.sort) {
|
|
195
|
+
child.query.sort = await getAllowedSort({ collection: relation.collection, relation, accountability: options.accountability }, context);
|
|
196
|
+
}
|
|
197
|
+
if (isO2MNode(child) && child.query.group && child.query.group[0] !== relation.field) {
|
|
198
|
+
// If a group by is used, the result needs to be grouped by the foreign key of the relation first, so results
|
|
199
|
+
// are correctly grouped under the foreign key when extracting the grouped results from the nested queries.
|
|
200
|
+
child.query.group.unshift(relation.field);
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
if (child) {
|
|
@@ -198,3 +213,6 @@ export async function parseFields(options, context) {
|
|
|
198
213
|
return true;
|
|
199
214
|
});
|
|
200
215
|
}
|
|
216
|
+
export function isO2MNode(node) {
|
|
217
|
+
return !!node && node.type === 'o2m';
|
|
218
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Accountability, Query, Relation } from '@directus/types';
|
|
2
|
+
import type { Context } from '../../../permissions/types.js';
|
|
3
|
+
export type GetAllowedSortFieldOptions = {
|
|
4
|
+
collection: string;
|
|
5
|
+
accountability: Accountability | null;
|
|
6
|
+
query?: Query;
|
|
7
|
+
relation?: Relation;
|
|
8
|
+
};
|
|
9
|
+
export declare function getAllowedSort(options: GetAllowedSortFieldOptions, context: Context): Promise<string[] | null>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
2
|
+
export async function getAllowedSort(options, context) {
|
|
3
|
+
// We'll default to the primary key for the standard sort output
|
|
4
|
+
let sortField = context.schema.collections[options.collection].primary;
|
|
5
|
+
// If a custom manual sort field is configured, use that
|
|
6
|
+
if (context.schema.collections[options.collection]?.sortField) {
|
|
7
|
+
sortField = context.schema.collections[options.collection].sortField;
|
|
8
|
+
}
|
|
9
|
+
// If a sort field is defined on the relation, use that
|
|
10
|
+
if (options.relation?.meta?.sort_field) {
|
|
11
|
+
sortField = options.relation.meta.sort_field;
|
|
12
|
+
}
|
|
13
|
+
if (options.accountability && options.accountability.admin === false) {
|
|
14
|
+
// Verify that the user has access to the sort field
|
|
15
|
+
const allowedFields = await fetchAllowedFields({
|
|
16
|
+
collection: options.collection,
|
|
17
|
+
action: 'read',
|
|
18
|
+
accountability: options.accountability,
|
|
19
|
+
}, context);
|
|
20
|
+
if (allowedFields.length === 0) {
|
|
21
|
+
sortField = null;
|
|
22
|
+
}
|
|
23
|
+
else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
|
|
24
|
+
// If the sort field is not allowed, default to the first allowed field
|
|
25
|
+
sortField = allowedFields[0];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// When group by is used, default to the first column provided in the group by clause
|
|
29
|
+
if (options.query?.group?.[0]) {
|
|
30
|
+
sortField = options.query.group[0];
|
|
31
|
+
}
|
|
32
|
+
if (sortField)
|
|
33
|
+
return [sortField];
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { Filter, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import { DatabaseHelper } from '../types.js';
|
|
4
4
|
export type FnHelperOptions = {
|
|
5
5
|
type: string | undefined;
|
|
6
|
-
query: Query | undefined;
|
|
7
|
-
cases: Filter[] | undefined;
|
|
8
6
|
originalCollectionName: string | undefined;
|
|
7
|
+
relationalCountOptions: {
|
|
8
|
+
query: Query;
|
|
9
|
+
cases: Filter[];
|
|
10
|
+
permissions: Permission[];
|
|
11
|
+
} | undefined;
|
|
9
12
|
};
|
|
10
13
|
export declare abstract class FnHelper extends DatabaseHelper {
|
|
11
14
|
protected schema: SchemaOverview;
|
|
@@ -20,7 +20,7 @@ export class FnHelper extends DatabaseHelper {
|
|
|
20
20
|
.count('*')
|
|
21
21
|
.from({ [alias]: relation.collection })
|
|
22
22
|
.where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));
|
|
23
|
-
if (options?.query
|
|
23
|
+
if (options?.relationalCountOptions?.query.filter) {
|
|
24
24
|
// set the newly aliased collection in the alias map as the default parent collection, indicated by '', for any nested filters
|
|
25
25
|
const aliasMap = {
|
|
26
26
|
'': {
|
|
@@ -28,7 +28,7 @@ export class FnHelper extends DatabaseHelper {
|
|
|
28
28
|
collection: relation.collection,
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
-
countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, aliasMap, options.cases
|
|
31
|
+
countQuery = applyFilter(this.knex, this.schema, countQuery, options.relationalCountOptions.query.filter, relation.collection, aliasMap, options.relationalCountOptions.cases, options.relationalCountOptions.permissions).query;
|
|
32
32
|
}
|
|
33
33
|
return this.knex.raw('(' + countQuery.toQuery() + ')');
|
|
34
34
|
}
|
|
@@ -8,10 +8,10 @@ import * as sequenceHelpers from './sequence/index.js';
|
|
|
8
8
|
import * as numberHelpers from './number/index.js';
|
|
9
9
|
export declare function getHelpers(database: Knex): {
|
|
10
10
|
date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
|
|
11
|
-
st: geometryHelpers.
|
|
12
|
-
schema: schemaHelpers.
|
|
11
|
+
st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
|
|
12
|
+
schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
|
|
13
13
|
sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
|
|
14
14
|
number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
|
|
15
15
|
};
|
|
16
|
-
export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.
|
|
16
|
+
export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
|
|
17
17
|
export type Helpers = ReturnType<typeof getHelpers>;
|
package/dist/database/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Knex } from 'knex';
|
|
|
3
3
|
import type { DatabaseClient } from '../types/index.js';
|
|
4
4
|
export default getDatabase;
|
|
5
5
|
export declare function getDatabase(): Knex;
|
|
6
|
-
export declare function getSchemaInspector(): SchemaInspector;
|
|
6
|
+
export declare function getSchemaInspector(database?: Knex): SchemaInspector;
|
|
7
7
|
/**
|
|
8
8
|
* Get database version. Value currently exists for MySQL only.
|
|
9
9
|
*
|
package/dist/database/index.js
CHANGED
|
@@ -143,11 +143,11 @@ export function getDatabase() {
|
|
|
143
143
|
});
|
|
144
144
|
return database;
|
|
145
145
|
}
|
|
146
|
-
export function getSchemaInspector() {
|
|
146
|
+
export function getSchemaInspector(database) {
|
|
147
147
|
if (inspector) {
|
|
148
148
|
return inspector;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
database ??= getDatabase();
|
|
151
151
|
inspector = createInspector(database);
|
|
152
152
|
return inspector;
|
|
153
153
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInspector } from '@directus/schema';
|
|
2
2
|
import { useLogger } from '../../logger/index.js';
|
|
3
|
+
import { getDatabaseClient } from '../index.js';
|
|
3
4
|
/**
|
|
4
5
|
* Things to keep in mind:
|
|
5
6
|
*
|
|
@@ -99,7 +100,7 @@ export async function up(knex) {
|
|
|
99
100
|
* MySQL won't delete the index when you drop the foreign key constraint. Gotta make
|
|
100
101
|
* sure to clean those up as well
|
|
101
102
|
*/
|
|
102
|
-
if (knex
|
|
103
|
+
if (getDatabaseClient(knex) === 'mysql') {
|
|
103
104
|
try {
|
|
104
105
|
await knex.schema.alterTable(update.table, (table) => {
|
|
105
106
|
// Knex uses a default convention for index names: `table_column_type`
|
|
@@ -140,7 +141,7 @@ export async function down(knex) {
|
|
|
140
141
|
* MySQL won't delete the index when you drop the foreign key constraint. Gotta make
|
|
141
142
|
* sure to clean those up as well
|
|
142
143
|
*/
|
|
143
|
-
if (knex
|
|
144
|
+
if (getDatabaseClient(knex) === 'mysql') {
|
|
144
145
|
try {
|
|
145
146
|
await knex.schema.alterTable(update.table, (table) => {
|
|
146
147
|
// Knex uses a default convention for index names: `table_column_type`
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { createInspector } from '@directus/schema';
|
|
2
2
|
import { useLogger } from '../../logger/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getDatabaseClient } from '../index.js';
|
|
4
4
|
export async function up(knex) {
|
|
5
|
-
const
|
|
6
|
-
const isMysql = helper.isOneOfClients(['mysql']);
|
|
5
|
+
const isMysql = getDatabaseClient(knex) === 'mysql';
|
|
7
6
|
if (isMysql) {
|
|
8
7
|
await dropConstraint(knex);
|
|
9
8
|
}
|
|
@@ -16,8 +15,7 @@ export async function up(knex) {
|
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
export async function down(knex) {
|
|
19
|
-
const
|
|
20
|
-
const isMysql = helper.isOneOfClients(['mysql']);
|
|
18
|
+
const isMysql = getDatabaseClient(knex) === 'mysql';
|
|
21
19
|
if (isMysql) {
|
|
22
20
|
await dropConstraint(knex);
|
|
23
21
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDatabaseClient } from '../index.js';
|
|
2
2
|
export async function up(knex) {
|
|
3
|
-
|
|
4
|
-
const isMysql = helper.isOneOfClients(['mysql']);
|
|
5
|
-
if (isMysql) {
|
|
3
|
+
if (getDatabaseClient(knex) === 'mysql') {
|
|
6
4
|
// Knex creates invalid statement on MySQL, see https://github.com/knex/knex/issues/1888
|
|
7
5
|
await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `uploaded_on` `created_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
|
|
8
6
|
}
|
|
@@ -20,9 +18,7 @@ export async function down(knex) {
|
|
|
20
18
|
await knex.schema.alterTable('directus_files', (table) => {
|
|
21
19
|
table.dropColumn('uploaded_on');
|
|
22
20
|
});
|
|
23
|
-
|
|
24
|
-
const isMysql = helper.isOneOfClients(['mysql']);
|
|
25
|
-
if (isMysql) {
|
|
21
|
+
if (getDatabaseClient(knex) === 'mysql') {
|
|
26
22
|
await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `created_on` `uploaded_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
|
|
27
23
|
}
|
|
28
24
|
else {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { processChunk, toBoolean } from '@directus/utils';
|
|
2
2
|
import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { useLogger } from '../../logger/index.js';
|
|
4
5
|
import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
|
|
5
6
|
import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
|
|
6
7
|
import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
|
|
7
8
|
import { getSchema } from '../../utils/get-schema.js';
|
|
9
|
+
import { getSchemaInspector } from '../index.js';
|
|
8
10
|
// Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
|
|
9
11
|
export function mergePermissions(strategy, ...permissions) {
|
|
10
12
|
const allPermissions = flatten(permissions);
|
|
@@ -129,6 +131,7 @@ async function fetchRoleAccess(roles, context) {
|
|
|
129
131
|
*/
|
|
130
132
|
const PUBLIC_POLICY_ID = 'abf8a154-5b1c-4a46-ac9c-7300570f4f17';
|
|
131
133
|
export async function up(knex) {
|
|
134
|
+
const logger = useLogger();
|
|
132
135
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
133
136
|
// If the policies table already exists the migration has already run
|
|
134
137
|
if (await knex.schema.hasTable('directus_policies')) {
|
|
@@ -183,10 +186,21 @@ export async function up(knex) {
|
|
|
183
186
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
184
187
|
// Link permissions to policies instead of roles
|
|
185
188
|
await knex.schema.alterTable('directus_permissions', (table) => {
|
|
186
|
-
table.uuid('policy')
|
|
187
|
-
// Drop the foreign key constraint here in order to update `null` role to public policy ID
|
|
188
|
-
table.dropForeign('role');
|
|
189
|
+
table.uuid('policy');
|
|
189
190
|
});
|
|
191
|
+
try {
|
|
192
|
+
const inspector = await getSchemaInspector(knex);
|
|
193
|
+
const foreignKeys = await inspector.foreignKeys('directus_permissions');
|
|
194
|
+
const foreignConstraint = foreignKeys.find((foreign) => foreign.foreign_key_table === 'directus_roles' && foreign.column === 'role')
|
|
195
|
+
?.constraint_name || undefined;
|
|
196
|
+
await knex.schema.alterTable('directus_permissions', (table) => {
|
|
197
|
+
// Drop the foreign key constraint here in order to update `null` role to public policy ID
|
|
198
|
+
table.dropForeign('role', foreignConstraint);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
logger.warn('Failed to drop foreign key constraint on `role` column in `directus_permissions` table');
|
|
203
|
+
}
|
|
190
204
|
await knex('directus_permissions')
|
|
191
205
|
.update({
|
|
192
206
|
role: PUBLIC_POLICY_ID,
|
|
@@ -198,6 +212,7 @@ export async function up(knex) {
|
|
|
198
212
|
await knex.schema.alterTable('directus_permissions', (table) => {
|
|
199
213
|
table.dropColumns('role');
|
|
200
214
|
table.dropNullable('policy');
|
|
215
|
+
table.foreign('policy').references('directus_policies.id').onDelete('CASCADE');
|
|
201
216
|
});
|
|
202
217
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
203
218
|
// Setup junction table between roles/users and policies
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
|
|
4
|
-
export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[]): Knex.QueryBuilder;
|
|
4
|
+
export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): Knex.QueryBuilder;
|
|
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
|
|
|
9
9
|
import { getNodeAlias } from '../utils/get-field-alias.js';
|
|
10
10
|
import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
|
|
11
11
|
import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
|
|
12
|
-
export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases) {
|
|
12
|
+
export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
|
|
13
13
|
const aliasMap = Object.create(null);
|
|
14
14
|
const env = useEnv();
|
|
15
|
-
const preProcess = getColumnPreprocessor(knex, schema, table, cases, aliasMap);
|
|
15
|
+
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
|
|
16
16
|
const queryCopy = cloneDeep(query);
|
|
17
17
|
const helpers = getHelpers(knex);
|
|
18
18
|
const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
|
|
@@ -25,7 +25,10 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
25
25
|
const groupWhenCases = hasCaseWhen
|
|
26
26
|
? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
|
|
27
27
|
: undefined;
|
|
28
|
-
const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases,
|
|
28
|
+
const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
|
|
29
|
+
aliasMap,
|
|
30
|
+
groupWhenCases,
|
|
31
|
+
}).query;
|
|
29
32
|
flatQuery.select(fieldNodes.map((node) => preProcess(node)));
|
|
30
33
|
withPreprocessBindings(knex, dbQuery);
|
|
31
34
|
return dbQuery;
|
|
@@ -42,7 +45,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
42
45
|
hasMultiRelationalSort = sortResult.hasMultiRelationalSort;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, {
|
|
48
|
+
const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, permissions, {
|
|
46
49
|
aliasMap,
|
|
47
50
|
isInnerQuery: true,
|
|
48
51
|
hasMultiRelationalSort,
|
|
@@ -68,6 +71,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
68
71
|
cases,
|
|
69
72
|
table,
|
|
70
73
|
alias: node.fieldKey,
|
|
74
|
+
permissions,
|
|
71
75
|
}, { knex, schema });
|
|
72
76
|
}));
|
|
73
77
|
}
|
|
@@ -159,7 +163,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
159
163
|
SELECT ...,
|
|
160
164
|
CASE WHEN `inner`.<random-prefix>_<alias> > 0 THEN <actual-column> END AS <alias>
|
|
161
165
|
*/
|
|
162
|
-
const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, innerCaseWhenAliasPrefix);
|
|
166
|
+
const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, innerCaseWhenAliasPrefix);
|
|
163
167
|
// To optimize the query we avoid having unnecessary columns in the inner query, that don't have a caseWhen, since
|
|
164
168
|
// they are selected in the outer query directly
|
|
165
169
|
dbQuery.select(fieldNodes.map(innerPreprocess).filter((x) => x !== null));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Item, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Accountability, Item, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { AST, NestedCollectionNode } from '../../types/ast.js';
|
|
3
3
|
import type { RunASTOptions } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Execute a given AST using Knex. Returns array of items based on requested AST.
|
|
6
6
|
*/
|
|
7
|
-
export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, options?: RunASTOptions): Promise<null | Item | Item[]>;
|
|
7
|
+
export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, accountability: Accountability | null, options?: RunASTOptions): Promise<null | Item | Item[]>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { cloneDeep, merge } from 'lodash-es';
|
|
3
|
+
import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
|
|
4
|
+
import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
|
|
3
5
|
import { PayloadService } from '../../services/payload.js';
|
|
4
6
|
import getDatabase from '../index.js';
|
|
5
7
|
import { getDBQuery } from './lib/get-db-query.js';
|
|
@@ -10,26 +12,31 @@ import { removeTemporaryFields } from './utils/remove-temporary-fields.js';
|
|
|
10
12
|
/**
|
|
11
13
|
* Execute a given AST using Knex. Returns array of items based on requested AST.
|
|
12
14
|
*/
|
|
13
|
-
export async function runAst(originalAST, schema, options) {
|
|
15
|
+
export async function runAst(originalAST, schema, accountability, options) {
|
|
14
16
|
const ast = cloneDeep(originalAST);
|
|
15
17
|
const knex = options?.knex || getDatabase();
|
|
16
18
|
if (ast.type === 'a2o') {
|
|
17
19
|
const results = {};
|
|
18
20
|
for (const collection of ast.names) {
|
|
19
|
-
results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? []);
|
|
21
|
+
results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? [], accountability);
|
|
20
22
|
}
|
|
21
23
|
return results;
|
|
22
24
|
}
|
|
23
25
|
else {
|
|
24
|
-
return await run(ast.name, ast.children, options?.query || ast.query, ast.cases);
|
|
26
|
+
return await run(ast.name, ast.children, options?.query || ast.query, ast.cases, accountability);
|
|
25
27
|
}
|
|
26
|
-
async function run(collection, children, query, cases) {
|
|
28
|
+
async function run(collection, children, query, cases, accountability) {
|
|
27
29
|
const env = useEnv();
|
|
28
30
|
// Retrieve the database columns to select in the current AST
|
|
29
31
|
const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(schema, collection, children, query);
|
|
30
32
|
const o2mNodes = nestedCollectionNodes.filter((node) => node.type === 'o2m');
|
|
33
|
+
let permissions = [];
|
|
34
|
+
if (accountability && !accountability.admin) {
|
|
35
|
+
const policies = await fetchPolicies(accountability, { schema, knex });
|
|
36
|
+
permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
|
|
37
|
+
}
|
|
31
38
|
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
|
32
|
-
const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases);
|
|
39
|
+
const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
|
|
33
40
|
const rawItems = await dbQuery;
|
|
34
41
|
if (!rawItems)
|
|
35
42
|
return null;
|
|
@@ -72,7 +79,7 @@ export async function runAst(originalAST, schema, options) {
|
|
|
72
79
|
page: null,
|
|
73
80
|
},
|
|
74
81
|
});
|
|
75
|
-
nestedItems = (await runAst(node, schema, { knex, nested: true }));
|
|
82
|
+
nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
|
|
76
83
|
if (nestedItems) {
|
|
77
84
|
items = mergeWithParentItems(schema, nestedItems, items, nestedNode, fieldAllowed);
|
|
78
85
|
}
|
|
@@ -86,7 +93,7 @@ export async function runAst(originalAST, schema, options) {
|
|
|
86
93
|
const node = merge({}, nestedNode, {
|
|
87
94
|
query: { limit: -1 },
|
|
88
95
|
});
|
|
89
|
-
nestedItems = (await runAst(node, schema, { knex, nested: true }));
|
|
96
|
+
nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
|
|
90
97
|
if (nestedItems) {
|
|
91
98
|
// Merge all fetched nested records with the parent items
|
|
92
99
|
items = mergeWithParentItems(schema, nestedItems, items, nestedNode, true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { AliasMap } from '../../../utils/get-column-path.js';
|
|
4
4
|
export interface ApplyCaseWhenOptions {
|
|
@@ -8,9 +8,10 @@ export interface ApplyCaseWhenOptions {
|
|
|
8
8
|
cases: Filter[];
|
|
9
9
|
aliasMap: AliasMap;
|
|
10
10
|
alias?: string;
|
|
11
|
+
permissions: Permission[];
|
|
11
12
|
}
|
|
12
13
|
export interface ApplyCaseWhenContext {
|
|
13
14
|
knex: Knex;
|
|
14
15
|
schema: SchemaOverview;
|
|
15
16
|
}
|
|
16
|
-
export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
|
|
17
|
+
export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { applyFilter } from '../../../utils/apply-query.js';
|
|
2
|
-
export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }, { knex, schema }) {
|
|
2
|
+
export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }, { knex, schema }) {
|
|
3
3
|
const caseQuery = knex.queryBuilder();
|
|
4
|
-
applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases);
|
|
4
|
+
applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases, permissions);
|
|
5
5
|
const compiler = knex.client.queryCompiler(caseQuery);
|
|
6
6
|
const sqlParts = [];
|
|
7
7
|
// Only empty filters, so no where was generated, skip it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { FieldNode, FunctionFieldNode, M2ONode } from '../../../types/ast.js';
|
|
4
4
|
import type { AliasMap } from '../../../utils/get-column-path.js';
|
|
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
|
|
|
6
6
|
/** Don't assign an alias to the column but instead return the column as is */
|
|
7
7
|
noAlias?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
9
|
+
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
10
10
|
export {};
|