@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.
Files changed (92) hide show
  1. package/dist/cli/utils/create-db-connection.js +3 -5
  2. package/dist/controllers/files.js +1 -1
  3. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +2 -2
  4. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  5. package/dist/database/get-ast-from-query/utils/get-related-collection.js +2 -2
  6. package/dist/database/helpers/fn/types.js +2 -1
  7. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -0
  8. package/dist/database/helpers/schema/dialects/mssql.js +6 -0
  9. package/dist/database/helpers/schema/dialects/mysql.d.ts +0 -1
  10. package/dist/database/helpers/schema/dialects/mysql.js +0 -11
  11. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -0
  12. package/dist/database/helpers/schema/dialects/oracle.js +6 -0
  13. package/dist/database/helpers/schema/types.d.ts +2 -1
  14. package/dist/database/helpers/schema/types.js +6 -4
  15. package/dist/database/index.d.ts +0 -6
  16. package/dist/database/index.js +12 -28
  17. package/dist/database/migrations/20210225A-add-relations-sort-field.js +3 -1
  18. package/dist/database/migrations/20210510A-restructure-relations.js +8 -8
  19. package/dist/database/migrations/20210924A-add-collection-organization.js +6 -1
  20. package/dist/database/migrations/20210927A-replace-fields-group.js +3 -1
  21. package/dist/database/migrations/20211118A-add-notifications.js +3 -1
  22. package/dist/database/migrations/20211211A-add-shares.js +7 -1
  23. package/dist/database/migrations/20230823A-add-content-versioning.js +3 -1
  24. package/dist/database/migrations/20240909A-separate-comments.js +3 -1
  25. package/dist/database/run-ast/lib/apply-query/add-join.d.ts +54 -0
  26. package/dist/database/run-ast/lib/apply-query/add-join.js +86 -0
  27. package/dist/database/run-ast/lib/apply-query/aggregate.d.ts +3 -0
  28. package/dist/database/run-ast/lib/apply-query/aggregate.js +24 -0
  29. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +8 -0
  30. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +20 -0
  31. package/dist/database/run-ast/lib/apply-query/filter/index.d.ts +8 -0
  32. package/dist/database/run-ast/lib/apply-query/filter/index.js +151 -0
  33. package/dist/database/run-ast/lib/apply-query/filter/operator.d.ts +3 -0
  34. package/dist/database/run-ast/lib/apply-query/filter/operator.js +175 -0
  35. package/dist/database/run-ast/lib/apply-query/filter/validate-operator.d.ts +2 -0
  36. package/dist/database/run-ast/lib/apply-query/filter/validate-operator.js +18 -0
  37. package/dist/database/run-ast/lib/apply-query/get-filter-path.d.ts +1 -0
  38. package/dist/database/run-ast/lib/apply-query/get-filter-path.js +13 -0
  39. package/dist/database/run-ast/lib/apply-query/get-operation.d.ts +7 -0
  40. package/dist/database/run-ast/lib/apply-query/get-operation.js +19 -0
  41. package/dist/database/run-ast/lib/apply-query/index.d.ts +20 -0
  42. package/dist/database/run-ast/lib/apply-query/index.js +92 -0
  43. package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.d.ts +2 -0
  44. package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.js +12 -0
  45. package/dist/database/run-ast/lib/apply-query/mock.d.ts +3 -0
  46. package/dist/database/run-ast/lib/apply-query/mock.js +4 -0
  47. package/dist/database/run-ast/lib/apply-query/pagination.d.ts +3 -0
  48. package/dist/database/run-ast/lib/apply-query/pagination.js +11 -0
  49. package/dist/database/run-ast/lib/apply-query/search.d.ts +4 -0
  50. package/dist/database/run-ast/lib/apply-query/search.js +78 -0
  51. package/dist/database/run-ast/lib/apply-query/sort.d.ts +19 -0
  52. package/dist/database/run-ast/lib/apply-query/sort.js +87 -0
  53. package/dist/database/run-ast/lib/get-db-query.js +7 -5
  54. package/dist/database/run-ast/run-ast.js +1 -1
  55. package/dist/database/run-ast/utils/apply-case-when.js +1 -1
  56. package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
  57. package/dist/{utils → database/run-ast/utils}/get-column.js +1 -1
  58. package/dist/database/run-ast/utils/get-field-alias.js +1 -1
  59. package/dist/database/run-ast/utils/remove-temporary-fields.js +1 -1
  60. package/dist/database/seeds/01-collections.yaml +2 -2
  61. package/dist/database/seeds/04-fields.yaml +2 -2
  62. package/dist/database/seeds/05-activity.yaml +1 -1
  63. package/dist/database/seeds/08-permissions.yaml +1 -1
  64. package/dist/database/seeds/09-presets.yaml +1 -1
  65. package/dist/database/seeds/10-relations.yaml +8 -8
  66. package/dist/database/seeds/11-revisions.yaml +1 -1
  67. package/dist/database/seeds/run.js +8 -1
  68. package/dist/flows.js +8 -1
  69. package/dist/metrics/lib/create-metrics.js +13 -7
  70. package/dist/operations/condition/index.js +7 -4
  71. package/dist/operations/exec/index.js +2 -1
  72. package/dist/permissions/modules/process-payload/process-payload.js +17 -11
  73. package/dist/permissions/utils/default-permission.d.ts +7 -0
  74. package/dist/permissions/utils/default-permission.js +7 -0
  75. package/dist/permissions/utils/fetch-dynamic-variable-data.js +3 -1
  76. package/dist/services/collections.js +22 -18
  77. package/dist/services/fields.js +2 -5
  78. package/dist/services/payload.d.ts +6 -6
  79. package/dist/services/payload.js +47 -13
  80. package/dist/services/server.js +7 -2
  81. package/dist/services/specifications.js +2 -2
  82. package/dist/utils/get-relation-info.d.ts +1 -1
  83. package/dist/utils/get-relation-info.js +2 -4
  84. package/dist/websocket/handlers/items.js +5 -0
  85. package/package.json +22 -20
  86. package/dist/database/get-ast-from-query/utils/get-relation.d.ts +0 -2
  87. package/dist/database/get-ast-from-query/utils/get-relation.js +0 -7
  88. package/dist/utils/apply-query.d.ts +0 -46
  89. package/dist/utils/apply-query.js +0 -773
  90. /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.d.ts +0 -0
  91. /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.js +0 -0
  92. /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 = async (conn, callback) => {
50
- const run = promisify(conn.query.bind(conn));
51
- await run('SET serial_normalization = "sql_sequence"');
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 = filename;
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 './get-relation.js';
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
  }
@@ -54,4 +54,10 @@ export class SchemaHelperMSSQL extends SchemaHelper {
54
54
  groupByFields.push(...sortRecords.map(({ column }) => column));
55
55
  }
56
56
  }
57
+ getColumnNameMaxLength() {
58
+ return 128;
59
+ }
60
+ getTableNameMaxLength() {
61
+ return 128;
62
+ }
57
63
  }
@@ -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
  }
@@ -96,4 +96,10 @@ export class SchemaHelperOracle extends SchemaHelper {
96
96
  groupByFields.push(...sortRecords.map(({ column }) => column));
97
97
  }
98
98
  }
99
+ getColumnNameMaxLength() {
100
+ return 128;
101
+ }
102
+ getTableNameMaxLength() {
103
+ return 128;
104
+ }
99
105
  }
@@ -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
  }
@@ -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;
@@ -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 = async (conn, callback) => {
93
+ poolConfig.afterCreate = (conn, callback) => {
96
94
  logger.trace('Enabling SQLite Foreign Keys support...');
97
- const run = promisify(conn.run.bind(conn));
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 = async (conn, callback) => {
100
+ poolConfig.afterCreate = (conn, callback) => {
104
101
  logger.trace('Setting CRDB serial_normalization and default_int_size');
105
- const run = promisify(conn.query.bind(conn));
106
- await run('SET serial_normalization = "sql_sequence"');
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 = async (conn, callback) => {
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
- await conn.executeAsync('ALTER SESSION SET NLS_TIMESTAMP_FORMAT = \'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"\'');
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
- await conn.executeAsync("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
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).schema;
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: 64,
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).schema;
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', 64);
25
- table.string('one_primary', 64);
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.string('group', 64).references('collection').inTable('directus_collections');
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', 64);
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', 64);
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.string('collection', 64).references('collection').inTable('directus_collections').onDelete('CASCADE');
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', 64)
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', 64).notNullable();
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,3 @@
1
+ import type { Aggregate, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string, hasJoins: boolean): void;
@@ -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
+ }