@directus/api 26.0.0 → 27.0.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/cli/utils/create-db-connection.js +3 -5
- package/dist/controllers/files.js +1 -1
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +2 -2
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/get-ast-from-query/utils/get-related-collection.js +2 -2
- package/dist/database/helpers/fn/types.js +2 -1
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -0
- package/dist/database/helpers/schema/dialects/mssql.js +6 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +0 -1
- package/dist/database/helpers/schema/dialects/mysql.js +0 -11
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -0
- package/dist/database/helpers/schema/dialects/oracle.js +6 -0
- package/dist/database/helpers/schema/types.d.ts +2 -1
- package/dist/database/helpers/schema/types.js +6 -4
- package/dist/database/index.d.ts +0 -6
- package/dist/database/index.js +12 -28
- package/dist/database/migrations/20210225A-add-relations-sort-field.js +3 -1
- package/dist/database/migrations/20210510A-restructure-relations.js +8 -8
- package/dist/database/migrations/20210924A-add-collection-organization.js +6 -1
- package/dist/database/migrations/20210927A-replace-fields-group.js +3 -1
- package/dist/database/migrations/20211118A-add-notifications.js +3 -1
- package/dist/database/migrations/20211211A-add-shares.js +7 -1
- package/dist/database/migrations/20230823A-add-content-versioning.js +3 -1
- package/dist/database/migrations/20240909A-separate-comments.js +3 -1
- package/dist/database/run-ast/lib/apply-query/add-join.d.ts +54 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +86 -0
- package/dist/database/run-ast/lib/apply-query/aggregate.d.ts +3 -0
- package/dist/database/run-ast/lib/apply-query/aggregate.js +24 -0
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +8 -0
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +20 -0
- package/dist/database/run-ast/lib/apply-query/filter/index.d.ts +8 -0
- package/dist/database/run-ast/lib/apply-query/filter/index.js +151 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.d.ts +3 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +175 -0
- package/dist/database/run-ast/lib/apply-query/filter/validate-operator.d.ts +2 -0
- package/dist/database/run-ast/lib/apply-query/filter/validate-operator.js +18 -0
- package/dist/database/run-ast/lib/apply-query/get-filter-path.d.ts +1 -0
- package/dist/database/run-ast/lib/apply-query/get-filter-path.js +13 -0
- package/dist/database/run-ast/lib/apply-query/get-operation.d.ts +7 -0
- package/dist/database/run-ast/lib/apply-query/get-operation.js +19 -0
- package/dist/database/run-ast/lib/apply-query/index.d.ts +20 -0
- package/dist/database/run-ast/lib/apply-query/index.js +92 -0
- package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.d.ts +2 -0
- package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.js +12 -0
- package/dist/database/run-ast/lib/apply-query/mock.d.ts +3 -0
- package/dist/database/run-ast/lib/apply-query/mock.js +4 -0
- package/dist/database/run-ast/lib/apply-query/pagination.d.ts +3 -0
- package/dist/database/run-ast/lib/apply-query/pagination.js +11 -0
- package/dist/database/run-ast/lib/apply-query/search.d.ts +4 -0
- package/dist/database/run-ast/lib/apply-query/search.js +78 -0
- package/dist/database/run-ast/lib/apply-query/sort.d.ts +19 -0
- package/dist/database/run-ast/lib/apply-query/sort.js +87 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -5
- package/dist/database/run-ast/run-ast.js +1 -1
- package/dist/database/run-ast/utils/apply-case-when.js +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
- package/dist/{utils → database/run-ast/utils}/get-column.js +1 -1
- package/dist/database/run-ast/utils/get-field-alias.js +1 -1
- package/dist/database/run-ast/utils/remove-temporary-fields.js +1 -1
- package/dist/database/seeds/01-collections.yaml +2 -2
- package/dist/database/seeds/04-fields.yaml +2 -2
- package/dist/database/seeds/05-activity.yaml +1 -1
- package/dist/database/seeds/08-permissions.yaml +1 -1
- package/dist/database/seeds/09-presets.yaml +1 -1
- package/dist/database/seeds/10-relations.yaml +8 -8
- package/dist/database/seeds/11-revisions.yaml +1 -1
- package/dist/database/seeds/run.js +8 -1
- package/dist/flows.js +8 -1
- package/dist/metrics/lib/create-metrics.js +13 -7
- package/dist/operations/condition/index.js +7 -4
- package/dist/operations/exec/index.js +2 -1
- package/dist/permissions/modules/process-payload/process-payload.js +17 -11
- package/dist/permissions/utils/default-permission.d.ts +7 -0
- package/dist/permissions/utils/default-permission.js +7 -0
- package/dist/permissions/utils/fetch-dynamic-variable-data.js +3 -1
- package/dist/services/collections.js +22 -18
- package/dist/services/fields.js +2 -5
- package/dist/services/payload.d.ts +6 -6
- package/dist/services/payload.js +47 -13
- package/dist/services/server.js +7 -2
- package/dist/services/specifications.js +2 -2
- package/dist/utils/get-relation-info.d.ts +1 -1
- package/dist/utils/get-relation-info.js +2 -4
- package/dist/websocket/handlers/items.js +5 -0
- package/package.json +22 -20
- package/dist/database/get-ast-from-query/utils/get-relation.d.ts +0 -2
- package/dist/database/get-ast-from-query/utils/get-relation.js +0 -7
- package/dist/utils/apply-query.d.ts +0 -46
- package/dist/utils/apply-query.js +0 -773
- /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.d.ts +0 -0
- /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.js +0 -0
- /package/dist/{utils → database/run-ast/utils}/get-column.d.ts +0 -0
|
@@ -2,7 +2,6 @@ import knex from 'knex';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { promisify } from 'util';
|
|
6
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
export default function createDBConnection(client, credentials) {
|
|
8
7
|
let connection = {};
|
|
@@ -46,10 +45,9 @@ export default function createDBConnection(client, credentials) {
|
|
|
46
45
|
knexConfig.useNullAsDefault = true;
|
|
47
46
|
}
|
|
48
47
|
if (client === 'cockroachdb') {
|
|
49
|
-
knexConfig.pool.afterCreate =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await run('SET default_int_size = 4');
|
|
48
|
+
knexConfig.pool.afterCreate = (conn, callback) => {
|
|
49
|
+
conn.query('SET serial_normalization = "sql_sequence"');
|
|
50
|
+
conn.query('SET default_int_size = 4');
|
|
53
51
|
callback(null, conn);
|
|
54
52
|
};
|
|
55
53
|
}
|
|
@@ -77,7 +77,7 @@ export const multipartHandler = (req, res, next) => {
|
|
|
77
77
|
payload.title = formatTitle(path.parse(filename).name);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
payload.filename_download
|
|
80
|
+
payload.filename_download ||= filename;
|
|
81
81
|
const payloadWithRequiredFields = {
|
|
82
82
|
...payload,
|
|
83
83
|
type: mimeType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { getRelation } from '@directus/utils';
|
|
1
2
|
import { cloneDeep } from 'lodash-es';
|
|
2
3
|
import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
3
|
-
import { getRelation } from '../utils/get-relation.js';
|
|
4
4
|
export async function convertWildcards(options, context) {
|
|
5
5
|
const fields = cloneDeep(options.fields);
|
|
6
6
|
const fieldsInCollection = Object.entries(context.schema.collections[options.parentCollection].fields).map(([name]) => name);
|
|
@@ -47,7 +47,7 @@ export async function convertWildcards(options, context) {
|
|
|
47
47
|
const isMany = relation.collection === options.parentCollection;
|
|
48
48
|
return isMany ? relation.field : relation.meta?.one_field;
|
|
49
49
|
})
|
|
50
|
-
: allowedFields.filter((fieldKey) => !!getRelation(context.schema, options.parentCollection, fieldKey));
|
|
50
|
+
: allowedFields.filter((fieldKey) => !!getRelation(context.schema.relations, options.parentCollection, fieldKey));
|
|
51
51
|
const nonRelationalFields = allowedFields.filter((fieldKey) => relationalFields.includes(fieldKey) === false);
|
|
52
52
|
const aliasFields = Object.keys(options.query.alias ?? {}).map((fieldKey) => {
|
|
53
53
|
const name = options.query.alias[fieldKey];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
|
|
2
|
+
import { getRelation } from '@directus/utils';
|
|
2
3
|
import { isEmpty } from 'lodash-es';
|
|
3
4
|
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
4
5
|
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
@@ -6,7 +7,6 @@ import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
|
6
7
|
import { getAllowedSort } from '../utils/get-allowed-sort.js';
|
|
7
8
|
import { getDeepQuery } from '../utils/get-deep-query.js';
|
|
8
9
|
import { getRelatedCollection } from '../utils/get-related-collection.js';
|
|
9
|
-
import { getRelation } from '../utils/get-relation.js';
|
|
10
10
|
import { convertWildcards } from './convert-wildcards.js';
|
|
11
11
|
export async function parseFields(options, context) {
|
|
12
12
|
let { fields } = options;
|
|
@@ -108,7 +108,7 @@ export async function parseFields(options, context) {
|
|
|
108
108
|
fieldName = options.query.alias[fieldKey];
|
|
109
109
|
}
|
|
110
110
|
const relatedCollection = getRelatedCollection(context.schema, options.parentCollection, fieldName);
|
|
111
|
-
const relation = getRelation(context.schema, options.parentCollection, fieldName);
|
|
111
|
+
const relation = getRelation(context.schema.relations, options.parentCollection, fieldName);
|
|
112
112
|
if (!relation)
|
|
113
113
|
continue;
|
|
114
114
|
const relationType = getRelationType({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getRelation } from '
|
|
1
|
+
import { getRelation } from '@directus/utils';
|
|
2
2
|
export function getRelatedCollection(schema, collection, field) {
|
|
3
|
-
const relation = getRelation(schema, collection, field);
|
|
3
|
+
const relation = getRelation(schema.relations, collection, field);
|
|
4
4
|
if (!relation)
|
|
5
5
|
return null;
|
|
6
6
|
if (relation.collection === collection && relation.field === field) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { applyFilter, generateAlias } from '../../../utils/apply-query.js';
|
|
2
1
|
import { DatabaseHelper } from '../types.js';
|
|
2
|
+
import { generateAlias } from '../../run-ast/lib/apply-query/index.js';
|
|
3
|
+
import { applyFilter } from '../../run-ast/lib/apply-query/filter/index.js';
|
|
3
4
|
export class FnHelper extends DatabaseHelper {
|
|
4
5
|
schema;
|
|
5
6
|
constructor(knex, schema) {
|
|
@@ -8,4 +8,6 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
8
8
|
getDatabaseSize(): Promise<number | null>;
|
|
9
9
|
prepQueryParams(queryParams: Sql): Sql;
|
|
10
10
|
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
11
|
+
getColumnNameMaxLength(): number;
|
|
12
|
+
getTableNameMaxLength(): number;
|
|
11
13
|
}
|
|
@@ -2,7 +2,6 @@ import type { Knex } from 'knex';
|
|
|
2
2
|
import { SchemaHelper, type SortRecord } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperMySQL extends SchemaHelper {
|
|
4
4
|
generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
|
|
5
|
-
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
6
5
|
getDatabaseSize(): Promise<number | null>;
|
|
7
6
|
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
8
7
|
}
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { getDefaultIndexName } from '../../../../utils/get-default-index-name.js';
|
|
3
|
-
import { getDatabaseVersion } from '../../../index.js';
|
|
4
3
|
import { SchemaHelper } from '../types.js';
|
|
5
4
|
const env = useEnv();
|
|
6
5
|
export class SchemaHelperMySQL extends SchemaHelper {
|
|
7
6
|
generateIndexName(type, collection, fields) {
|
|
8
7
|
return getDefaultIndexName(type, collection, fields, { maxLength: 64 });
|
|
9
8
|
}
|
|
10
|
-
applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields) {
|
|
11
|
-
if (getDatabaseVersion()?.startsWith('5.7')) {
|
|
12
|
-
dbQuery.orderByRaw(`?? asc, ${orderByString}`, [`${table}.${primaryKey}`, ...orderByFields]);
|
|
13
|
-
dbQuery = knex
|
|
14
|
-
.select(knex.raw(`??, ( @rank := IF ( @cur_id = deep.${primaryKey}, @rank + 1, 1 ) ) AS directus_row_number, ( @cur_id := deep.${primaryKey} ) AS current_id`, 'deep.*'))
|
|
15
|
-
.from(knex.raw('? as ??, (SELECT @rank := 0, @cur_id := null) vars', [dbQuery, 'deep']));
|
|
16
|
-
return dbQuery;
|
|
17
|
-
}
|
|
18
|
-
return super.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
|
|
19
|
-
}
|
|
20
9
|
async getDatabaseSize() {
|
|
21
10
|
try {
|
|
22
11
|
const result = (await this.knex
|
|
@@ -18,4 +18,6 @@ export declare class SchemaHelperOracle extends SchemaHelper {
|
|
|
18
18
|
prepQueryParams(queryParams: Sql): Sql;
|
|
19
19
|
prepBindings(bindings: Knex.Value[]): any;
|
|
20
20
|
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
21
|
+
getColumnNameMaxLength(): number;
|
|
22
|
+
getTableNameMaxLength(): number;
|
|
21
23
|
}
|
|
@@ -32,7 +32,6 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
32
32
|
applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
|
|
33
33
|
applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
|
|
34
34
|
castA2oPrimaryKey(): string;
|
|
35
|
-
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
36
35
|
formatUUID(uuid: string): string;
|
|
37
36
|
/**
|
|
38
37
|
* @returns Size of the database in bytes
|
|
@@ -41,4 +40,6 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
41
40
|
prepQueryParams(queryParams: Sql): Sql;
|
|
42
41
|
prepBindings(bindings: Knex.Value[]): any;
|
|
43
42
|
addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
43
|
+
getColumnNameMaxLength(): number;
|
|
44
|
+
getTableNameMaxLength(): number;
|
|
44
45
|
}
|
|
@@ -94,10 +94,6 @@ export class SchemaHelper extends DatabaseHelper {
|
|
|
94
94
|
castA2oPrimaryKey() {
|
|
95
95
|
return 'CAST(?? AS CHAR(255))';
|
|
96
96
|
}
|
|
97
|
-
applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields) {
|
|
98
|
-
dbQuery.rowNumber(knex.ref('directus_row_number').toQuery(), knex.raw(`partition by ?? order by ${orderByString}`, [`${table}.${primaryKey}`, ...orderByFields]));
|
|
99
|
-
return dbQuery;
|
|
100
|
-
}
|
|
101
97
|
formatUUID(uuid) {
|
|
102
98
|
return uuid; // no-op by default
|
|
103
99
|
}
|
|
@@ -116,4 +112,10 @@ export class SchemaHelper extends DatabaseHelper {
|
|
|
116
112
|
addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasRelationalSort) {
|
|
117
113
|
// no-op by default
|
|
118
114
|
}
|
|
115
|
+
getColumnNameMaxLength() {
|
|
116
|
+
return 64;
|
|
117
|
+
}
|
|
118
|
+
getTableNameMaxLength() {
|
|
119
|
+
return 64;
|
|
120
|
+
}
|
|
119
121
|
}
|
package/dist/database/index.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ import type { DatabaseClient } from '../types/index.js';
|
|
|
4
4
|
export default getDatabase;
|
|
5
5
|
export declare function getDatabase(): Knex;
|
|
6
6
|
export declare function getSchemaInspector(database?: Knex): SchemaInspector;
|
|
7
|
-
/**
|
|
8
|
-
* Get database version. Value currently exists for MySQL only.
|
|
9
|
-
*
|
|
10
|
-
* @returns Cached database version
|
|
11
|
-
*/
|
|
12
|
-
export declare function getDatabaseVersion(): string | null;
|
|
13
7
|
export declare function hasDatabaseConnection(database?: Knex): Promise<boolean>;
|
|
14
8
|
export declare function validateDatabaseConnection(database?: Knex): Promise<void>;
|
|
15
9
|
export declare function getDatabaseClient(database?: Knex): DatabaseClient;
|
package/dist/database/index.js
CHANGED
|
@@ -3,12 +3,11 @@ import { createInspector } from '@directus/schema';
|
|
|
3
3
|
import { isObject } from '@directus/utils';
|
|
4
4
|
import fse from 'fs-extra';
|
|
5
5
|
import knex from 'knex';
|
|
6
|
-
import { isArray, merge } from 'lodash-es';
|
|
6
|
+
import { isArray, merge, toArray } from 'lodash-es';
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { performance } from 'perf_hooks';
|
|
11
|
-
import { promisify } from 'util';
|
|
12
11
|
import { getExtensionsPath } from '../extensions/lib/get-extensions-path.js';
|
|
13
12
|
import { useLogger } from '../logger/index.js';
|
|
14
13
|
import { useMetrics } from '../metrics/index.js';
|
|
@@ -17,7 +16,6 @@ import { validateEnv } from '../utils/validate-env.js';
|
|
|
17
16
|
import { getHelpers } from './helpers/index.js';
|
|
18
17
|
let database = null;
|
|
19
18
|
let inspector = null;
|
|
20
|
-
let databaseVersion = null;
|
|
21
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
20
|
export default getDatabase;
|
|
23
21
|
export function getDatabase() {
|
|
@@ -92,29 +90,27 @@ export function getDatabase() {
|
|
|
92
90
|
};
|
|
93
91
|
if (client === 'sqlite3') {
|
|
94
92
|
knexConfig.useNullAsDefault = true;
|
|
95
|
-
poolConfig.afterCreate =
|
|
93
|
+
poolConfig.afterCreate = (conn, callback) => {
|
|
96
94
|
logger.trace('Enabling SQLite Foreign Keys support...');
|
|
97
|
-
|
|
98
|
-
await run('PRAGMA foreign_keys = ON');
|
|
95
|
+
conn.run('PRAGMA foreign_keys = ON');
|
|
99
96
|
callback(null, conn);
|
|
100
97
|
};
|
|
101
98
|
}
|
|
102
99
|
if (client === 'cockroachdb') {
|
|
103
|
-
poolConfig.afterCreate =
|
|
100
|
+
poolConfig.afterCreate = (conn, callback) => {
|
|
104
101
|
logger.trace('Setting CRDB serial_normalization and default_int_size');
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await run('SET default_int_size = 4');
|
|
102
|
+
conn.query('SET serial_normalization = "sql_sequence"');
|
|
103
|
+
conn.query('SET default_int_size = 4');
|
|
108
104
|
callback(null, conn);
|
|
109
105
|
};
|
|
110
106
|
}
|
|
111
107
|
if (client === 'oracledb') {
|
|
112
|
-
poolConfig.afterCreate =
|
|
108
|
+
poolConfig.afterCreate = (conn, callback) => {
|
|
113
109
|
logger.trace('Setting OracleDB NLS_DATE_FORMAT and NLS_TIMESTAMP_FORMAT');
|
|
114
110
|
// enforce proper ISO standard 2024-12-10T10:54:00.123Z for datetime/timestamp
|
|
115
|
-
|
|
111
|
+
conn.execute('ALTER SESSION SET NLS_TIMESTAMP_FORMAT = \'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"\'');
|
|
116
112
|
// enforce 2024-12-10 date formet
|
|
117
|
-
|
|
113
|
+
conn.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
|
|
118
114
|
callback(null, conn);
|
|
119
115
|
};
|
|
120
116
|
}
|
|
@@ -123,13 +119,6 @@ export function getDatabase() {
|
|
|
123
119
|
if (isObject(knexConfig.connection))
|
|
124
120
|
delete knexConfig.connection['filename'];
|
|
125
121
|
Object.assign(knexConfig, { client: 'mysql2' });
|
|
126
|
-
poolConfig.afterCreate = async (conn, callback) => {
|
|
127
|
-
logger.trace('Retrieving database version');
|
|
128
|
-
const run = promisify(conn.query.bind(conn));
|
|
129
|
-
const version = await run('SELECT @@version;');
|
|
130
|
-
databaseVersion = version[0]['@@version'];
|
|
131
|
-
callback(null, conn);
|
|
132
|
-
};
|
|
133
122
|
}
|
|
134
123
|
if (client === 'mssql') {
|
|
135
124
|
// This brings MS SQL in line with the other DB vendors. We shouldn't do any automatic
|
|
@@ -173,14 +162,6 @@ export function getSchemaInspector(database) {
|
|
|
173
162
|
inspector = createInspector(database);
|
|
174
163
|
return inspector;
|
|
175
164
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Get database version. Value currently exists for MySQL only.
|
|
178
|
-
*
|
|
179
|
-
* @returns Cached database version
|
|
180
|
-
*/
|
|
181
|
-
export function getDatabaseVersion() {
|
|
182
|
-
return databaseVersion;
|
|
183
|
-
}
|
|
184
165
|
export async function hasDatabaseConnection(database) {
|
|
185
166
|
database = database ?? getDatabase();
|
|
186
167
|
try {
|
|
@@ -296,8 +277,11 @@ async function validateDatabaseCharset(database) {
|
|
|
296
277
|
.select({ table_name: 'TABLE_NAME', name: 'COLUMN_NAME', collation: 'COLLATION_NAME' })
|
|
297
278
|
.where({ TABLE_SCHEMA: env['DB_DATABASE'] })
|
|
298
279
|
.whereNot({ COLLATION_NAME: collation });
|
|
280
|
+
const excludedTables = toArray(env['DB_EXCLUDE_TABLES']);
|
|
299
281
|
let inconsistencies = '';
|
|
300
282
|
for (const table of tables) {
|
|
283
|
+
if (excludedTables.includes(table.name))
|
|
284
|
+
continue;
|
|
301
285
|
const tableColumns = columns.filter((column) => column.table_name === table.name);
|
|
302
286
|
const tableHasInvalidCollation = table.collation !== collation;
|
|
303
287
|
if (tableHasInvalidCollation || tableColumns.length > 0) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { parseJSON } from '@directus/utils';
|
|
2
|
+
import { getHelpers } from '../helpers/index.js';
|
|
2
3
|
export async function up(knex) {
|
|
4
|
+
const helpers = getHelpers(knex);
|
|
3
5
|
await knex.schema.alterTable('directus_relations', (table) => {
|
|
4
|
-
table.string('sort_field');
|
|
6
|
+
table.string('sort_field', helpers.schema.getColumnNameMaxLength());
|
|
5
7
|
});
|
|
6
8
|
const fieldsWithSort = await knex
|
|
7
9
|
.select('collection', 'field', 'options')
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { getHelpers } from '../helpers/index.js';
|
|
2
2
|
export async function up(knex) {
|
|
3
|
-
const helper = getHelpers(knex)
|
|
3
|
+
const helper = getHelpers(knex);
|
|
4
4
|
await knex.schema.alterTable('directus_relations', (table) => {
|
|
5
5
|
table.dropColumns('many_primary', 'one_primary');
|
|
6
6
|
table.string('one_deselect_action').defaultTo('nullify');
|
|
7
7
|
});
|
|
8
8
|
await knex('directus_relations').update({ one_deselect_action: 'nullify' });
|
|
9
|
-
await helper.changeToType('directus_relations', 'sort_field', 'string', {
|
|
10
|
-
length:
|
|
9
|
+
await helper.schema.changeToType('directus_relations', 'sort_field', 'string', {
|
|
10
|
+
length: helper.schema.getColumnNameMaxLength(),
|
|
11
11
|
});
|
|
12
|
-
await helper.changeToType('directus_relations', 'one_deselect_action', 'string', {
|
|
12
|
+
await helper.schema.changeToType('directus_relations', 'one_deselect_action', 'string', {
|
|
13
13
|
nullable: false,
|
|
14
14
|
default: 'nullify',
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
export async function down(knex) {
|
|
18
|
-
const helper = getHelpers(knex)
|
|
19
|
-
await helper.changeToType('directus_relations', 'sort_field', 'string', {
|
|
18
|
+
const helper = getHelpers(knex);
|
|
19
|
+
await helper.schema.changeToType('directus_relations', 'sort_field', 'string', {
|
|
20
20
|
length: 255,
|
|
21
21
|
});
|
|
22
22
|
await knex.schema.alterTable('directus_relations', (table) => {
|
|
23
23
|
table.dropColumn('one_deselect_action');
|
|
24
|
-
table.string('many_primary',
|
|
25
|
-
table.string('one_primary',
|
|
24
|
+
table.string('many_primary', helper.schema.getColumnNameMaxLength());
|
|
25
|
+
table.string('one_primary', helper.schema.getColumnNameMaxLength());
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { getHelpers } from '../helpers/index.js';
|
|
1
2
|
export async function up(knex) {
|
|
3
|
+
const helpers = getHelpers(knex);
|
|
2
4
|
await knex.schema.alterTable('directus_collections', (table) => {
|
|
3
5
|
table.integer('sort');
|
|
4
|
-
table
|
|
6
|
+
table
|
|
7
|
+
.string('group', helpers.schema.getTableNameMaxLength())
|
|
8
|
+
.references('collection')
|
|
9
|
+
.inTable('directus_collections');
|
|
5
10
|
table.string('collapse').defaultTo('open').notNullable();
|
|
6
11
|
});
|
|
7
12
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { uniq } from 'lodash-es';
|
|
2
|
+
import { getHelpers } from '../helpers/index.js';
|
|
2
3
|
export async function up(knex) {
|
|
4
|
+
const helpers = getHelpers(knex);
|
|
3
5
|
const groupsInUse = await knex.select('id', 'group').from('directus_fields').whereNotNull('group');
|
|
4
6
|
const groupIDs = uniq(groupsInUse.map(({ group }) => group));
|
|
5
7
|
const groupFields = await knex.select('id', 'field').from('directus_fields').whereIn('id', groupIDs);
|
|
@@ -14,7 +16,7 @@ export async function up(knex) {
|
|
|
14
16
|
table.dropColumn('group');
|
|
15
17
|
});
|
|
16
18
|
await knex.schema.alterTable('directus_fields', (table) => {
|
|
17
|
-
table.string('group',
|
|
19
|
+
table.string('group', helpers.schema.getColumnNameMaxLength());
|
|
18
20
|
});
|
|
19
21
|
for (const { id, group } of groupsInUse) {
|
|
20
22
|
await knex('directus_fields')
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { getHelpers } from '../helpers/index.js';
|
|
1
2
|
export async function up(knex) {
|
|
3
|
+
const helpers = getHelpers(knex);
|
|
2
4
|
await knex.schema.createTable('directus_notifications', (table) => {
|
|
3
5
|
table.increments();
|
|
4
6
|
table.timestamp('timestamp').notNullable();
|
|
@@ -7,7 +9,7 @@ export async function up(knex) {
|
|
|
7
9
|
table.uuid('sender').notNullable().references('id').inTable('directus_users');
|
|
8
10
|
table.string('subject').notNullable();
|
|
9
11
|
table.text('message');
|
|
10
|
-
table.string('collection',
|
|
12
|
+
table.string('collection', helpers.schema.getTableNameMaxLength());
|
|
11
13
|
table.string('item');
|
|
12
14
|
});
|
|
13
15
|
await knex.schema.alterTable('directus_users', (table) => {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { getHelpers } from '../helpers/index.js';
|
|
1
2
|
export async function up(knex) {
|
|
3
|
+
const helpers = getHelpers(knex);
|
|
2
4
|
await knex.schema.createTable('directus_shares', (table) => {
|
|
3
5
|
table.uuid('id').primary().notNullable();
|
|
4
6
|
table.string('name');
|
|
5
|
-
table
|
|
7
|
+
table
|
|
8
|
+
.string('collection', helpers.schema.getTableNameMaxLength())
|
|
9
|
+
.references('collection')
|
|
10
|
+
.inTable('directus_collections')
|
|
11
|
+
.onDelete('CASCADE');
|
|
6
12
|
table.string('item');
|
|
7
13
|
table.uuid('role').references('id').inTable('directus_roles').onDelete('CASCADE');
|
|
8
14
|
table.string('password');
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { getHelpers } from '../helpers/index.js';
|
|
1
2
|
export async function up(knex) {
|
|
3
|
+
const helpers = getHelpers(knex);
|
|
2
4
|
await knex.schema.createTable('directus_versions', (table) => {
|
|
3
5
|
table.uuid('id').primary().notNullable();
|
|
4
6
|
table.string('key', 64).notNullable();
|
|
5
7
|
table.string('name');
|
|
6
8
|
table
|
|
7
|
-
.string('collection',
|
|
9
|
+
.string('collection', helpers.schema.getTableNameMaxLength())
|
|
8
10
|
.notNullable()
|
|
9
11
|
.references('collection')
|
|
10
12
|
.inTable('directus_collections')
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Action } from '@directus/constants';
|
|
2
|
+
import { getHelpers } from '../helpers/index.js';
|
|
2
3
|
export async function up(knex) {
|
|
4
|
+
const helpers = getHelpers(knex);
|
|
3
5
|
await knex.schema.createTable('directus_comments', (table) => {
|
|
4
6
|
table.uuid('id').primary().notNullable();
|
|
5
|
-
table.string('collection',
|
|
7
|
+
table.string('collection', helpers.schema.getTableNameMaxLength()).notNullable();
|
|
6
8
|
table.string('item').notNullable();
|
|
7
9
|
table.text('comment').notNullable();
|
|
8
10
|
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import type { AliasMap } from '../../../../utils/get-column-path.js';
|
|
4
|
+
/**
|
|
5
|
+
* Apply a given filter object to the Knex QueryBuilder instance.
|
|
6
|
+
*
|
|
7
|
+
* Relational nested filters, like the following example:
|
|
8
|
+
*
|
|
9
|
+
* ```json
|
|
10
|
+
* // Fetch pages that have articles written by Rijk
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* "articles": {
|
|
14
|
+
* "author": {
|
|
15
|
+
* "name": {
|
|
16
|
+
* "_eq": "Rijk"
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* are handled by joining the nested tables, and using a where statement on the top level on the
|
|
24
|
+
* nested field through the join. This allows us to filter the top level items based on nested data.
|
|
25
|
+
* The where on the root is done with a subquery to prevent duplicates, any nested joins are done
|
|
26
|
+
* with aliases to prevent naming conflicts.
|
|
27
|
+
*
|
|
28
|
+
* The output SQL for the above would look something like:
|
|
29
|
+
*
|
|
30
|
+
* ```sql
|
|
31
|
+
* SELECT *
|
|
32
|
+
* FROM pages
|
|
33
|
+
* WHERE
|
|
34
|
+
* pages.id in (
|
|
35
|
+
* SELECT articles.page_id AS page_id
|
|
36
|
+
* FROM articles
|
|
37
|
+
* LEFT JOIN authors AS xviqp ON articles.author = xviqp.id
|
|
38
|
+
* WHERE xviqp.name = 'Rijk'
|
|
39
|
+
* )
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
type AddJoinProps = {
|
|
43
|
+
path: string[];
|
|
44
|
+
collection: string;
|
|
45
|
+
aliasMap: AliasMap;
|
|
46
|
+
rootQuery: Knex.QueryBuilder;
|
|
47
|
+
schema: SchemaOverview;
|
|
48
|
+
knex: Knex;
|
|
49
|
+
};
|
|
50
|
+
export declare function addJoin({ path, collection, aliasMap, rootQuery, schema, knex }: AddJoinProps): {
|
|
51
|
+
hasMultiRelational: boolean;
|
|
52
|
+
isJoinAdded: boolean;
|
|
53
|
+
};
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
+
import { clone } from 'lodash-es';
|
|
3
|
+
import { getRelationInfo } from '../../../../utils/get-relation-info.js';
|
|
4
|
+
import { getHelpers } from '../../../helpers/index.js';
|
|
5
|
+
import { generateAlias } from './index.js';
|
|
6
|
+
export function addJoin({ path, collection, aliasMap, rootQuery, schema, knex }) {
|
|
7
|
+
let hasMultiRelational = false;
|
|
8
|
+
let isJoinAdded = false;
|
|
9
|
+
path = clone(path);
|
|
10
|
+
followRelation(path);
|
|
11
|
+
return { hasMultiRelational, isJoinAdded };
|
|
12
|
+
function followRelation(pathParts, parentCollection = collection, parentFields) {
|
|
13
|
+
/**
|
|
14
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
15
|
+
*/
|
|
16
|
+
const pathRoot = pathParts[0].split(':')[0];
|
|
17
|
+
const { relation, relationType } = getRelationInfo(schema.relations, parentCollection, pathRoot);
|
|
18
|
+
if (!relation) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const existingAlias = parentFields
|
|
22
|
+
? aliasMap[`${parentFields}.${pathParts[0]}`]?.alias
|
|
23
|
+
: aliasMap[pathParts[0]]?.alias;
|
|
24
|
+
if (!existingAlias) {
|
|
25
|
+
const alias = generateAlias();
|
|
26
|
+
const aliasKey = parentFields ? `${parentFields}.${pathParts[0]}` : pathParts[0];
|
|
27
|
+
const aliasedParentCollection = aliasMap[parentFields ?? '']?.alias || parentCollection;
|
|
28
|
+
aliasMap[aliasKey] = { alias, collection: '' };
|
|
29
|
+
if (relationType === 'm2o') {
|
|
30
|
+
rootQuery.leftJoin({ [alias]: relation.related_collection }, `${aliasedParentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
|
|
31
|
+
aliasMap[aliasKey].collection = relation.related_collection;
|
|
32
|
+
isJoinAdded = true;
|
|
33
|
+
}
|
|
34
|
+
else if (relationType === 'a2o') {
|
|
35
|
+
const pathScope = pathParts[0].split(':')[1];
|
|
36
|
+
if (!pathScope) {
|
|
37
|
+
throw new InvalidQueryError({
|
|
38
|
+
reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
|
|
42
|
+
joinClause
|
|
43
|
+
.onVal(`${aliasedParentCollection}.${relation.meta.one_collection_field}`, '=', pathScope)
|
|
44
|
+
.andOn(`${aliasedParentCollection}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${alias}.${schema.collections[pathScope].primary}`));
|
|
45
|
+
});
|
|
46
|
+
aliasMap[aliasKey].collection = pathScope;
|
|
47
|
+
isJoinAdded = true;
|
|
48
|
+
}
|
|
49
|
+
else if (relationType === 'o2a') {
|
|
50
|
+
rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
|
|
51
|
+
joinClause
|
|
52
|
+
.onVal(`${alias}.${relation.meta.one_collection_field}`, '=', parentCollection)
|
|
53
|
+
.andOn(`${alias}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${aliasedParentCollection}.${schema.collections[parentCollection].primary}`));
|
|
54
|
+
});
|
|
55
|
+
aliasMap[aliasKey].collection = relation.collection;
|
|
56
|
+
hasMultiRelational = true;
|
|
57
|
+
isJoinAdded = true;
|
|
58
|
+
}
|
|
59
|
+
else if (relationType === 'o2m') {
|
|
60
|
+
rootQuery.leftJoin({ [alias]: relation.collection }, `${aliasedParentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
|
|
61
|
+
aliasMap[aliasKey].collection = relation.collection;
|
|
62
|
+
hasMultiRelational = true;
|
|
63
|
+
isJoinAdded = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let parent;
|
|
67
|
+
if (relationType === 'm2o') {
|
|
68
|
+
parent = relation.related_collection;
|
|
69
|
+
}
|
|
70
|
+
else if (relationType === 'a2o') {
|
|
71
|
+
const pathScope = pathParts[0].split(':')[1];
|
|
72
|
+
if (!pathScope) {
|
|
73
|
+
throw new InvalidQueryError({
|
|
74
|
+
reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
parent = pathScope;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
parent = relation.collection;
|
|
81
|
+
}
|
|
82
|
+
if (pathParts.length > 1) {
|
|
83
|
+
followRelation(pathParts.slice(1), parent, `${parentFields ? parentFields + '.' : ''}${pathParts[0]}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins) {
|
|
2
|
+
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
3
|
+
if (!fields)
|
|
4
|
+
continue;
|
|
5
|
+
for (const field of fields) {
|
|
6
|
+
if (operation === 'countAll') {
|
|
7
|
+
dbQuery.count('*', { as: 'countAll' });
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (operation === 'count' && field === '*') {
|
|
11
|
+
dbQuery.count('*', { as: 'count' });
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (operation === 'countDistinct' && !hasJoins && schema.collections[collection]?.primary === field) {
|
|
15
|
+
// Optimize to count as primary keys are unique
|
|
16
|
+
dbQuery.count(`${collection}.${field}`, { as: `countDistinct->${field}` });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (['count', 'countDistinct', 'avg', 'avgDistinct', 'sum', 'sumDistinct', 'min', 'max'].includes(operation)) {
|
|
20
|
+
dbQuery[operation](`${collection}.${field}`, { as: `${operation}->${field}` });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|