@directus/api 22.1.0 → 22.2.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/dist/app.js +1 -1
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +2 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- 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 +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
- package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
- package/dist/database/helpers/nullable-update/index.d.ts +7 -0
- package/dist/database/helpers/nullable-update/index.js +7 -0
- package/dist/database/helpers/nullable-update/types.d.ts +7 -0
- package/dist/database/helpers/nullable-update/types.js +12 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +20 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +33 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/oracle.js +21 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/postgres.js +23 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
- package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
- package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +23 -13
- 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/extensions/manager.js +2 -2
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +11 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/middleware/respond.js +1 -0
- 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/request/is-denied-ip.js +7 -1
- package/dist/server.js +4 -2
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +66 -25
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/mail/index.js +1 -5
- package/dist/services/meta.js +8 -7
- package/dist/services/notifications.d.ts +0 -4
- package/dist/services/notifications.js +8 -6
- package/dist/services/permissions.js +19 -19
- package/dist/services/server.js +8 -1
- package/dist/services/specifications.js +7 -7
- package/dist/services/users.js +4 -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 +1 -1
- package/dist/utils/get-address.js +6 -1
- package/dist/utils/get-allowed-log-levels.d.ts +3 -0
- package/dist/utils/get-allowed-log-levels.js +11 -0
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/get-schema.js +19 -24
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/controllers/base.d.ts +10 -10
- package/dist/websocket/controllers/base.js +22 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -0
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +18 -0
- package/dist/websocket/controllers/logs.js +50 -0
- package/dist/websocket/controllers/rest.js +3 -1
- package/dist/websocket/handlers/index.d.ts +1 -0
- package/dist/websocket/handlers/index.js +21 -3
- package/dist/websocket/handlers/logs.d.ts +31 -0
- package/dist/websocket/handlers/logs.js +121 -0
- package/dist/websocket/messages.d.ts +26 -0
- package/dist/websocket/messages.js +9 -0
- package/dist/websocket/types.d.ts +7 -0
- package/package.json +27 -26
package/dist/app.js
CHANGED
|
@@ -125,7 +125,7 @@ export default async function createApp() {
|
|
|
125
125
|
'https://avatars.githubusercontent.com',
|
|
126
126
|
],
|
|
127
127
|
mediaSrc: ["'self'"],
|
|
128
|
-
connectSrc: ["'self'", 'https://*'],
|
|
128
|
+
connectSrc: ["'self'", 'https://*', 'wss://*'],
|
|
129
129
|
},
|
|
130
130
|
}, getConfigFromEnv('CONTENT_SECURITY_POLICY_'))));
|
|
131
131
|
if (env['HSTS_ENABLED']) {
|
package/dist/cache.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function clearSystemCache(opts?: {
|
|
|
13
13
|
}): Promise<void>;
|
|
14
14
|
export declare function setSystemCache(key: string, value: any, ttl?: number): Promise<void>;
|
|
15
15
|
export declare function getSystemCache(key: string): Promise<Record<string, any>>;
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
16
|
+
export declare function setLocalSchemaCache(schema: SchemaOverview): Promise<void>;
|
|
17
|
+
export declare function getLocalSchemaCache(): Promise<SchemaOverview | undefined>;
|
|
18
18
|
export declare function setCacheValue(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl?: number): Promise<void>;
|
|
19
19
|
export declare function getCacheValue(cache: Keyv, key: string): Promise<any>;
|
package/dist/cache.js
CHANGED
|
@@ -75,11 +75,11 @@ export async function getSystemCache(key) {
|
|
|
75
75
|
const { systemCache } = getCache();
|
|
76
76
|
return await getCacheValue(systemCache, key);
|
|
77
77
|
}
|
|
78
|
-
export async function
|
|
78
|
+
export async function setLocalSchemaCache(schema) {
|
|
79
79
|
const { localSchemaCache } = getCache();
|
|
80
80
|
await localSchemaCache.set('schema', schema);
|
|
81
81
|
}
|
|
82
|
-
export async function
|
|
82
|
+
export async function getLocalSchemaCache() {
|
|
83
83
|
const { localSchemaCache } = getCache();
|
|
84
84
|
return await localSchemaCache.get('schema');
|
|
85
85
|
}
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -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
|
}
|
|
@@ -6,12 +6,14 @@ import * as geometryHelpers from './geometry/index.js';
|
|
|
6
6
|
import * as schemaHelpers from './schema/index.js';
|
|
7
7
|
import * as sequenceHelpers from './sequence/index.js';
|
|
8
8
|
import * as numberHelpers from './number/index.js';
|
|
9
|
+
import * as nullableUpdateHelper from './nullable-update/index.js';
|
|
9
10
|
export declare function getHelpers(database: Knex): {
|
|
10
11
|
date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
|
|
11
12
|
st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
|
|
12
13
|
schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
|
|
13
14
|
sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
|
|
14
15
|
number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
|
|
16
|
+
nullableUpdate: nullableUpdateHelper.postgres | nullableUpdateHelper.oracle;
|
|
15
17
|
};
|
|
16
18
|
export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
|
|
17
19
|
export type Helpers = ReturnType<typeof getHelpers>;
|
|
@@ -5,6 +5,7 @@ import * as geometryHelpers from './geometry/index.js';
|
|
|
5
5
|
import * as schemaHelpers from './schema/index.js';
|
|
6
6
|
import * as sequenceHelpers from './sequence/index.js';
|
|
7
7
|
import * as numberHelpers from './number/index.js';
|
|
8
|
+
import * as nullableUpdateHelper from './nullable-update/index.js';
|
|
8
9
|
export function getHelpers(database) {
|
|
9
10
|
const client = getDatabaseClient(database);
|
|
10
11
|
return {
|
|
@@ -13,6 +14,7 @@ export function getHelpers(database) {
|
|
|
13
14
|
schema: new schemaHelpers[client](database),
|
|
14
15
|
sequence: new sequenceHelpers[client](database),
|
|
15
16
|
number: new numberHelpers[client](database),
|
|
17
|
+
nullableUpdate: new nullableUpdateHelper[client](database),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
export function getFunctions(database, schema) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Column } from '@directus/schema';
|
|
2
|
+
import type { Field, RawField } from '@directus/types';
|
|
3
|
+
import type { Knex } from 'knex';
|
|
4
|
+
import { NullableFieldUpdateHelper } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Oracle throws an error when overwriting the nullable option with same value.
|
|
7
|
+
* Therefore we need to check if the nullable option has changed and only then apply it.
|
|
8
|
+
* The default value can be set regardless of the previous value.
|
|
9
|
+
*/
|
|
10
|
+
export declare class NullableFieldUpdateHelperOracle extends NullableFieldUpdateHelper {
|
|
11
|
+
updateNullableValue(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NullableFieldUpdateHelper } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Oracle throws an error when overwriting the nullable option with same value.
|
|
4
|
+
* Therefore we need to check if the nullable option has changed and only then apply it.
|
|
5
|
+
* The default value can be set regardless of the previous value.
|
|
6
|
+
*/
|
|
7
|
+
export class NullableFieldUpdateHelperOracle extends NullableFieldUpdateHelper {
|
|
8
|
+
updateNullableValue(column, field, existing) {
|
|
9
|
+
if (field.schema?.is_nullable === false && existing.is_nullable === true) {
|
|
10
|
+
column.notNullable();
|
|
11
|
+
}
|
|
12
|
+
else if (field.schema?.is_nullable === true && existing.is_nullable === false) {
|
|
13
|
+
column.nullable();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { NullableFieldUpdateHelperOracle as oracle } from './dialects/oracle.js';
|
|
2
|
+
export { NullableFieldUpdateHelperDefault as postgres } from './dialects/default.js';
|
|
3
|
+
export { NullableFieldUpdateHelperDefault as mysql } from './dialects/default.js';
|
|
4
|
+
export { NullableFieldUpdateHelperDefault as cockroachdb } from './dialects/default.js';
|
|
5
|
+
export { NullableFieldUpdateHelperDefault as redshift } from './dialects/default.js';
|
|
6
|
+
export { NullableFieldUpdateHelperDefault as sqlite } from './dialects/default.js';
|
|
7
|
+
export { NullableFieldUpdateHelperDefault as mssql } from './dialects/default.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { NullableFieldUpdateHelperOracle as oracle } from './dialects/oracle.js';
|
|
2
|
+
export { NullableFieldUpdateHelperDefault as postgres } from './dialects/default.js';
|
|
3
|
+
export { NullableFieldUpdateHelperDefault as mysql } from './dialects/default.js';
|
|
4
|
+
export { NullableFieldUpdateHelperDefault as cockroachdb } from './dialects/default.js';
|
|
5
|
+
export { NullableFieldUpdateHelperDefault as redshift } from './dialects/default.js';
|
|
6
|
+
export { NullableFieldUpdateHelperDefault as sqlite } from './dialects/default.js';
|
|
7
|
+
export { NullableFieldUpdateHelperDefault as mssql } from './dialects/default.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import { DatabaseHelper } from '../types.js';
|
|
3
|
+
import type { Column } from '@directus/schema';
|
|
4
|
+
import type { Field, RawField } from '@directus/types';
|
|
5
|
+
export declare class NullableFieldUpdateHelper extends DatabaseHelper {
|
|
6
|
+
updateNullableValue(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DatabaseHelper } from '../types.js';
|
|
2
|
+
export class NullableFieldUpdateHelper extends DatabaseHelper {
|
|
3
|
+
updateNullableValue(column, field, existing) {
|
|
4
|
+
const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
|
|
5
|
+
if (isNullable) {
|
|
6
|
+
column.nullable();
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
column.notNullable();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { KNEX_TYPES } from '@directus/constants';
|
|
2
|
-
import
|
|
2
|
+
import { type Knex } from 'knex';
|
|
3
|
+
import type { Options, SortRecord, Sql } from '../types.js';
|
|
3
4
|
import { SchemaHelper } from '../types.js';
|
|
4
5
|
export declare class SchemaHelperCockroachDb extends SchemaHelper {
|
|
5
6
|
changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
|
|
6
7
|
constraintName(existingName: string): string;
|
|
7
8
|
getDatabaseSize(): Promise<number | null>;
|
|
8
9
|
preprocessBindings(queryParams: Sql): Sql;
|
|
10
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
|
|
9
11
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {} from 'knex';
|
|
1
2
|
import { SchemaHelper } from '../types.js';
|
|
2
3
|
import { useEnv } from '@directus/env';
|
|
3
4
|
import { preprocessBindings } from '../utils/preprocess-bindings.js';
|
|
@@ -31,4 +32,20 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
31
32
|
preprocessBindings(queryParams) {
|
|
32
33
|
return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
|
|
33
34
|
}
|
|
35
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
|
|
36
|
+
if (hasMultiRelationalSort) {
|
|
37
|
+
/*
|
|
38
|
+
Cockroach allows aliases to be used in the GROUP BY clause and only needs columns in the GROUP BY clause that
|
|
39
|
+
are not functionally dependent on the primary key.
|
|
40
|
+
|
|
41
|
+
> You can group columns by an alias (i.e., a label assigned to the column with an AS clause) rather than the column name.
|
|
42
|
+
|
|
43
|
+
> If aggregate groups are created on a full primary key, any column in the table can be selected as a target_elem,
|
|
44
|
+
or specified in a HAVING clause.
|
|
45
|
+
|
|
46
|
+
https://www.cockroachlabs.com/docs/stable/select-clause#parameters
|
|
47
|
+
*/
|
|
48
|
+
groupByFields.push(...sortRecords.map(({ alias }) => alias));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
34
51
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
|
-
import { SchemaHelper, type Sql } from '../types.js';
|
|
2
|
+
import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperMSSQL extends SchemaHelper {
|
|
4
4
|
applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
|
|
5
5
|
applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
|
|
6
6
|
formatUUID(uuid: string): string;
|
|
7
7
|
getDatabaseSize(): Promise<number | null>;
|
|
8
8
|
preprocessBindings(queryParams: Sql): Sql;
|
|
9
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
|
|
9
10
|
}
|
|
@@ -30,4 +30,24 @@ export class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
30
30
|
preprocessBindings(queryParams) {
|
|
31
31
|
return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
|
|
32
32
|
}
|
|
33
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
|
|
34
|
+
/*
|
|
35
|
+
MSSQL requires all selected columns that are not aggregated over are to be present in the GROUP BY clause
|
|
36
|
+
|
|
37
|
+
> When the select list has no aggregations, each column in the select list must be included in the GROUP BY list.
|
|
38
|
+
|
|
39
|
+
https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver16#g-syntax-variations-for-group-by
|
|
40
|
+
|
|
41
|
+
MSSQL does not support aliases in the GROUP BY clause
|
|
42
|
+
|
|
43
|
+
> The column expression cannot contain:
|
|
44
|
+
A column alias that is defined in the SELECT list. It can use a column alias for a derived table that is defined
|
|
45
|
+
in the FROM clause.
|
|
46
|
+
|
|
47
|
+
https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver16
|
|
48
|
+
*/
|
|
49
|
+
if (sortRecords.length > 0) {
|
|
50
|
+
groupByFields.push(...sortRecords.map(({ column }) => column));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
33
53
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
|
-
import { SchemaHelper } from '../types.js';
|
|
2
|
+
import { SchemaHelper, type SortRecord } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperMySQL extends SchemaHelper {
|
|
4
4
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
5
5
|
getDatabaseSize(): Promise<number | null>;
|
|
6
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
|
|
6
7
|
}
|
|
@@ -28,4 +28,37 @@ export class SchemaHelperMySQL extends SchemaHelper {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
|
|
32
|
+
if (hasMultiRelationalSort) {
|
|
33
|
+
/*
|
|
34
|
+
** MySQL **
|
|
35
|
+
|
|
36
|
+
MySQL only requires all selected sort columns that are not functionally dependent on the primary key to be included.
|
|
37
|
+
|
|
38
|
+
> If the ONLY_FULL_GROUP_BY SQL mode is enabled (which it is by default),
|
|
39
|
+
MySQL rejects queries for which the select list, HAVING condition, or ORDER BY list refer to
|
|
40
|
+
nonaggregated columns that are neither named in the GROUP BY clause nor are functionally dependent on them.
|
|
41
|
+
|
|
42
|
+
https://dev.mysql.com/doc/refman/8.4/en/group-by-handling.html
|
|
43
|
+
|
|
44
|
+
MySQL allows aliases to be used in the GROUP BY clause
|
|
45
|
+
|
|
46
|
+
> You can use the alias in GROUP BY, ORDER BY, or HAVING clauses to refer to the column:
|
|
47
|
+
|
|
48
|
+
https://dev.mysql.com/doc/refman/8.4/en/problems-with-alias.html
|
|
49
|
+
|
|
50
|
+
** MariaDB **
|
|
51
|
+
|
|
52
|
+
MariaDB does not document how it supports functional dependent columns in GROUP BY clauses.
|
|
53
|
+
But testing shows that it does support the same features as MySQL in this area.
|
|
54
|
+
|
|
55
|
+
MariaDB allows aliases to be used in the GROUP BY clause
|
|
56
|
+
|
|
57
|
+
> The GROUP BY expression can be a computed value, and can refer back to an identifer specified with AS.
|
|
58
|
+
|
|
59
|
+
https://mariadb.com/kb/en/group-by/#group-by-examples
|
|
60
|
+
*/
|
|
61
|
+
groupByFields.push(...sortRecords.map(({ alias }) => alias));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
31
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KNEX_TYPES } from '@directus/constants';
|
|
2
2
|
import type { Field, Relation, Type } from '@directus/types';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Knex } from 'knex';
|
|
4
|
+
import type { Options, SortRecord, Sql } from '../types.js';
|
|
4
5
|
import { SchemaHelper } from '../types.js';
|
|
5
6
|
export declare class SchemaHelperOracle extends SchemaHelper {
|
|
6
7
|
changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
|
|
@@ -9,4 +10,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
|
|
|
9
10
|
processFieldType(field: Field): Type;
|
|
10
11
|
getDatabaseSize(): Promise<number | null>;
|
|
11
12
|
preprocessBindings(queryParams: Sql): Sql;
|
|
13
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
|
|
12
14
|
}
|
|
@@ -42,4 +42,25 @@ export class SchemaHelperOracle extends SchemaHelper {
|
|
|
42
42
|
preprocessBindings(queryParams) {
|
|
43
43
|
return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
|
|
44
44
|
}
|
|
45
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
|
|
46
|
+
/*
|
|
47
|
+
Oracle requires all selected columns that are not aggregated over to be present in the GROUP BY clause
|
|
48
|
+
aliases can not be used before version 23c.
|
|
49
|
+
|
|
50
|
+
> If you also specify a group_by_clause in this statement, then this select list can contain only the following
|
|
51
|
+
types of expressions:
|
|
52
|
+
* Constants
|
|
53
|
+
* Aggregate functions and the functions USER, UID, and SYSDATE
|
|
54
|
+
* Expressions identical to those in the group_by_clause. If the group_by_clause is in a subquery,
|
|
55
|
+
then all columns in the select list of the subquery must match the GROUP BY columns in the subquery.
|
|
56
|
+
If the select list and GROUP BY columns of a top-level query or of a subquery do not match,
|
|
57
|
+
then the statement results in ORA-00979.
|
|
58
|
+
* Expressions involving the preceding expressions that evaluate to the same value for all rows in a group
|
|
59
|
+
|
|
60
|
+
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html
|
|
61
|
+
*/
|
|
62
|
+
if (sortRecords.length > 0) {
|
|
63
|
+
groupByFields.push(...sortRecords.map(({ column }) => column));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
|
|
2
3
|
export declare class SchemaHelperPostgres extends SchemaHelper {
|
|
3
4
|
getDatabaseSize(): Promise<number | null>;
|
|
4
5
|
preprocessBindings(queryParams: Sql): Sql;
|
|
6
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
|
|
5
7
|
}
|
|
@@ -15,4 +15,27 @@ export class SchemaHelperPostgres extends SchemaHelper {
|
|
|
15
15
|
preprocessBindings(queryParams) {
|
|
16
16
|
return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
|
|
17
17
|
}
|
|
18
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
|
|
19
|
+
if (hasMultiRelationalSort) {
|
|
20
|
+
/*
|
|
21
|
+
Postgres only requires selected columns that are not functionally dependent on the primary key to be
|
|
22
|
+
included in the GROUP BY clause. Since the results are already grouped by the primary key, we don't need to
|
|
23
|
+
always include the sort columns in the GROUP BY but only if there is a multi relational sort involved, eg.
|
|
24
|
+
a sort column that comes from a related M2O relation.
|
|
25
|
+
|
|
26
|
+
> When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list
|
|
27
|
+
expressions to refer to ungrouped columns except within aggregate functions or when the ungrouped column is
|
|
28
|
+
functionally dependent on the grouped columns, since there would otherwise be more than one possible value to
|
|
29
|
+
return for an ungrouped column.
|
|
30
|
+
https://www.postgresql.org/docs/current/sql-select.html
|
|
31
|
+
|
|
32
|
+
Postgres allows aliases to be used in the GROUP BY clause
|
|
33
|
+
|
|
34
|
+
> In strict SQL, GROUP BY can only group by columns of the source table but PostgreSQL extends this to also allow
|
|
35
|
+
GROUP BY to group by columns in the select list.
|
|
36
|
+
https://www.postgresql.org/docs/16/queries-table-expressions.html#QUERIES-GROUP
|
|
37
|
+
*/
|
|
38
|
+
groupByFields.push(...sortRecords.map(({ alias }) => alias));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
18
41
|
}
|
|
@@ -12,6 +12,10 @@ export type Sql = {
|
|
|
12
12
|
sql: string;
|
|
13
13
|
bindings: readonly Knex.Value[];
|
|
14
14
|
};
|
|
15
|
+
export type SortRecord = {
|
|
16
|
+
alias: string;
|
|
17
|
+
column: Knex.Raw;
|
|
18
|
+
};
|
|
15
19
|
export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
16
20
|
isOneOfClients(clients: DatabaseClient[]): boolean;
|
|
17
21
|
changeNullable(table: string, column: string, nullable: boolean): Promise<void>;
|
|
@@ -32,4 +36,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
32
36
|
*/
|
|
33
37
|
getDatabaseSize(): Promise<number | null>;
|
|
34
38
|
preprocessBindings(queryParams: Sql): Sql;
|
|
39
|
+
addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
|
|
35
40
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
1
2
|
import type { Sql } from '../types.js';
|
|
2
3
|
export type PreprocessBindingsOptions = {
|
|
3
4
|
format(index: number): string;
|
|
4
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Preprocess a SQL query, such that repeated binding values are bound to the same binding index.
|
|
8
|
+
**/
|
|
5
9
|
export declare function preprocessBindings(queryParams: (Partial<Sql> & Pick<Sql, 'sql'>) | string, options: PreprocessBindingsOptions): {
|
|
6
10
|
sql: string;
|
|
7
|
-
bindings:
|
|
11
|
+
bindings: Knex.Value[];
|
|
8
12
|
};
|