@directus/api 31.0.0 → 32.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 (135) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +11 -8
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +11 -8
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/server.js +26 -1
  20. package/dist/controllers/settings.js +9 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/database/helpers/fn/types.js +3 -3
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  33. package/dist/database/helpers/schema/types.d.ts +5 -0
  34. package/dist/database/helpers/schema/types.js +6 -0
  35. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  36. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  37. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  38. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  39. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  40. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  41. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  43. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  44. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  45. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  46. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  47. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  48. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  49. package/dist/flows.js +1 -0
  50. package/dist/mcp/schema.d.ts +14 -14
  51. package/dist/mcp/schema.js +6 -6
  52. package/dist/mcp/server.d.ts +9 -3
  53. package/dist/mcp/server.js +1 -1
  54. package/dist/mcp/tools/collections.d.ts +1 -1
  55. package/dist/mcp/tools/fields.d.ts +1 -1
  56. package/dist/mcp/tools/files.d.ts +25 -25
  57. package/dist/mcp/tools/flows.d.ts +36 -36
  58. package/dist/mcp/tools/folders.d.ts +18 -18
  59. package/dist/mcp/tools/items.d.ts +18 -18
  60. package/dist/mcp/tools/operations.d.ts +19 -19
  61. package/dist/mcp/tools/prompts/items.md +1 -1
  62. package/dist/metrics/lib/create-metrics.js +16 -25
  63. package/dist/middleware/collection-exists.js +2 -2
  64. package/dist/operations/mail/index.js +3 -1
  65. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  66. package/dist/operations/mail/rate-limiter.js +29 -0
  67. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  68. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  69. package/dist/schedules/metrics.js +6 -2
  70. package/dist/schedules/project.d.ts +4 -0
  71. package/dist/schedules/project.js +27 -0
  72. package/dist/services/collections.d.ts +3 -3
  73. package/dist/services/collections.js +16 -1
  74. package/dist/services/fields.d.ts +21 -5
  75. package/dist/services/fields.js +105 -28
  76. package/dist/services/graphql/resolvers/query.js +1 -1
  77. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  78. package/dist/services/graphql/schema/parse-query.js +8 -8
  79. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  80. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  81. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  82. package/dist/services/import-export.d.ts +9 -1
  83. package/dist/services/import-export.js +287 -101
  84. package/dist/services/items.d.ts +1 -1
  85. package/dist/services/items.js +36 -20
  86. package/dist/services/mail/index.js +2 -0
  87. package/dist/services/mail/rate-limiter.d.ts +1 -0
  88. package/dist/services/mail/rate-limiter.js +29 -0
  89. package/dist/services/meta.js +28 -24
  90. package/dist/services/schema.js +4 -1
  91. package/dist/services/server.d.ts +1 -0
  92. package/dist/services/server.js +14 -18
  93. package/dist/services/settings.d.ts +2 -1
  94. package/dist/services/settings.js +15 -0
  95. package/dist/services/tus/server.js +14 -9
  96. package/dist/telemetry/lib/get-report.js +4 -4
  97. package/dist/telemetry/lib/send-report.d.ts +6 -1
  98. package/dist/telemetry/lib/send-report.js +3 -1
  99. package/dist/telemetry/types/report.d.ts +17 -1
  100. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  101. package/dist/telemetry/utils/get-settings.js +14 -0
  102. package/dist/test-utils/README.md +760 -0
  103. package/dist/test-utils/cache.d.ts +51 -0
  104. package/dist/test-utils/cache.js +59 -0
  105. package/dist/test-utils/database.d.ts +48 -0
  106. package/dist/test-utils/database.js +52 -0
  107. package/dist/test-utils/emitter.d.ts +35 -0
  108. package/dist/test-utils/emitter.js +38 -0
  109. package/dist/test-utils/fields-service.d.ts +28 -0
  110. package/dist/test-utils/fields-service.js +36 -0
  111. package/dist/test-utils/items-service.d.ts +23 -0
  112. package/dist/test-utils/items-service.js +37 -0
  113. package/dist/test-utils/knex.d.ts +164 -0
  114. package/dist/test-utils/knex.js +268 -0
  115. package/dist/test-utils/schema.d.ts +26 -0
  116. package/dist/test-utils/schema.js +35 -0
  117. package/dist/types/auth.d.ts +0 -2
  118. package/dist/utils/apply-diff.js +15 -0
  119. package/dist/utils/create-admin.d.ts +11 -0
  120. package/dist/utils/create-admin.js +50 -0
  121. package/dist/utils/get-schema.js +5 -3
  122. package/dist/utils/get-snapshot-diff.js +49 -5
  123. package/dist/utils/get-snapshot.js +13 -7
  124. package/dist/utils/sanitize-schema.d.ts +11 -4
  125. package/dist/utils/sanitize-schema.js +9 -6
  126. package/dist/utils/schedule.js +15 -19
  127. package/dist/utils/validate-diff.js +31 -0
  128. package/dist/utils/validate-snapshot.js +7 -0
  129. package/dist/websocket/controllers/hooks.js +12 -20
  130. package/dist/websocket/messages.d.ts +3 -3
  131. package/package.json +63 -65
  132. package/dist/cli/utils/defaults.d.ts +0 -4
  133. package/dist/cli/utils/defaults.js +0 -17
  134. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  135. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -1,10 +1,12 @@
1
- import { RouteNotFoundError } from '@directus/errors';
1
+ import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
2
2
  import { format } from 'date-fns';
3
3
  import { Router } from 'express';
4
4
  import { respond } from '../middleware/respond.js';
5
+ import { SettingsService } from '../services/index.js';
5
6
  import { ServerService } from '../services/server.js';
6
7
  import { SpecificationService } from '../services/specifications.js';
7
8
  import asyncHandler from '../utils/async-handler.js';
9
+ import { createAdmin } from '../utils/create-admin.js';
8
10
  const router = Router();
9
11
  router.get('/specs/oas', asyncHandler(async (req, res, next) => {
10
12
  const service = new SpecificationService({
@@ -54,4 +56,27 @@ router.get('/health', asyncHandler(async (req, res, next) => {
54
56
  res.locals['cache'] = false;
55
57
  return next();
56
58
  }), respond);
59
+ router.post('/setup', asyncHandler(async (req, _res, next) => {
60
+ const serverService = new ServerService({ schema: req.schema });
61
+ if (await serverService.isSetupCompleted()) {
62
+ throw new ForbiddenError();
63
+ }
64
+ try {
65
+ await createAdmin(req.schema, {
66
+ email: req.body.project_owner,
67
+ password: req.body.password,
68
+ first_name: req.body.first_name,
69
+ last_name: req.body.last_name,
70
+ });
71
+ const settingsService = new SettingsService({ schema: req.schema });
72
+ settingsService.setOwner(req.body);
73
+ }
74
+ catch (error) {
75
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
76
+ return next();
77
+ }
78
+ throw error;
79
+ }
80
+ return next();
81
+ }), respond);
57
82
  export default router;
@@ -1,6 +1,5 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, isDirectusError } from '@directus/errors';
2
2
  import express from 'express';
3
- import { ErrorCode } from '@directus/errors';
4
3
  import { respond } from '../middleware/respond.js';
5
4
  import useCollection from '../middleware/use-collection.js';
6
5
  import { SettingsService } from '../services/settings.js';
@@ -16,6 +15,14 @@ router.get('/', asyncHandler(async (req, res, next) => {
16
15
  res.locals['payload'] = { data: records || null };
17
16
  return next();
18
17
  }), respond);
18
+ router.post('/owner', asyncHandler(async (req, _res, next) => {
19
+ const service = new SettingsService({
20
+ accountability: req.accountability,
21
+ schema: req.schema,
22
+ });
23
+ await service.setOwner(req.body);
24
+ return next();
25
+ }), respond);
19
26
  router.patch('/', asyncHandler(async (req, res, next) => {
20
27
  const service = new SettingsService({
21
28
  accountability: req.accountability,
@@ -1,6 +1,8 @@
1
1
  import { ErrorCode, ForbiddenError, InvalidCredentialsError, InvalidPayloadError, isDirectusError, } from '@directus/errors';
2
2
  import express from 'express';
3
3
  import Joi from 'joi';
4
+ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
5
+ import { getDatabase } from '../database/index.js';
4
6
  import checkRateLimit from '../middleware/rate-limiter-registration.js';
5
7
  import { respond } from '../middleware/respond.js';
6
8
  import useCollection from '../middleware/use-collection.js';
@@ -11,8 +13,6 @@ import { TFAService } from '../services/tfa.js';
11
13
  import { UsersService } from '../services/users.js';
12
14
  import asyncHandler from '../utils/async-handler.js';
13
15
  import { sanitizeQuery } from '../utils/sanitize-query.js';
14
- import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
15
- import { getDatabase } from '../database/index.js';
16
16
  const router = express.Router();
17
17
  router.use(useCollection('directus_users'));
18
18
  router.post('/', asyncHandler(async (req, res, next) => {
@@ -1,6 +1,6 @@
1
- import { DatabaseHelper } from '../types.js';
2
- import { generateAlias } from '../../run-ast/lib/apply-query/index.js';
3
1
  import { applyFilter } from '../../run-ast/lib/apply-query/filter/index.js';
2
+ import { generateRelationalQueryAlias } from '../../run-ast/utils/generate-alias.js';
3
+ import { DatabaseHelper } from '../types.js';
4
4
  export class FnHelper extends DatabaseHelper {
5
5
  schema;
6
6
  constructor(knex, schema) {
@@ -16,7 +16,7 @@ export class FnHelper extends DatabaseHelper {
16
16
  throw new Error(`Field ${collectionName}.${column} isn't a nested relational collection`);
17
17
  }
18
18
  // generate a unique alias for the relation collection, to prevent collisions in self referencing relations
19
- const alias = generateAlias();
19
+ const alias = generateRelationalQueryAlias(table, column, collectionName, options);
20
20
  let countQuery = this.knex
21
21
  .count('*')
22
22
  .from({ [alias]: relation.collection })
@@ -1,10 +1,11 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
2
  import { type Knex } from 'knex';
3
- import type { Options, SortRecord } from '../types.js';
3
+ import type { CreateIndexOptions, Options, SortRecord } from '../types.js';
4
4
  import { SchemaHelper } from '../types.js';
5
5
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
6
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
9
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
10
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
10
11
  }
@@ -44,4 +44,17 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
44
44
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
45
45
  }
46
46
  }
47
+ async createIndex(collection, field, options = {}) {
48
+ const isUnique = Boolean(options.unique);
49
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
50
+ // https://www.cockroachlabs.com/docs/stable/create-index
51
+ if (options.attemptConcurrentIndex) {
52
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX CONCURRENTLY ?? ON ?? (??)`, [
53
+ constraintName,
54
+ collection,
55
+ field,
56
+ ]);
57
+ }
58
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
59
+ }
47
60
  }
@@ -1,5 +1,5 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperMSSQL extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
@@ -10,4 +10,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
10
10
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
11
11
  getColumnNameMaxLength(): number;
12
12
  getTableNameMaxLength(): number;
13
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
13
14
  }
@@ -60,4 +60,27 @@ export class SchemaHelperMSSQL extends SchemaHelper {
60
60
  getTableNameMaxLength() {
61
61
  return 128;
62
62
  }
63
+ async createIndex(collection, field, options = {}) {
64
+ const isUnique = Boolean(options.unique);
65
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
66
+ /*
67
+ Online index operations are not available in every edition of Microsoft SQL Server.
68
+ For a list of features that are supported by the editions of SQL Server, see Editions and supported features of SQL Server 2022.
69
+
70
+ https://learn.microsoft.com/en-us/sql/sql-server/editions-and-components-of-sql-server-2022?view=sql-server-ver16#rdbms-high-availability
71
+ */
72
+ const edition = await this.knex
73
+ .raw(`SELECT SERVERPROPERTY('edition') AS edition`)
74
+ .then((data) => data?.[0]?.['edition']);
75
+ if (options.attemptConcurrentIndex && typeof edition === 'string' && edition.startsWith('Enterprise')) {
76
+ // https://learn.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver16#online---on--off-
77
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) WITH (ONLINE = ON)`, [
78
+ constraintName,
79
+ collection,
80
+ field,
81
+ ]);
82
+ }
83
+ // Fall back to blocking index creation for non-enterprise editions
84
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
85
+ }
63
86
  }
@@ -1,7 +1,8 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, 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
5
  getDatabaseSize(): Promise<number | null>;
6
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
7
8
  }
@@ -54,4 +54,29 @@ export class SchemaHelperMySQL extends SchemaHelper {
54
54
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
55
55
  }
56
56
  }
57
+ async createIndex(collection, field, options = {}) {
58
+ const isUnique = Boolean(options.unique);
59
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
60
+ const blockingQuery = this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [
61
+ constraintName,
62
+ collection,
63
+ field,
64
+ ]);
65
+ if (options.attemptConcurrentIndex) {
66
+ /*
67
+ Seems it is not possible to determine whether "ALGORITHM=INPLACE LOCK=NONE" will be supported
68
+ so we're just going to send it and fall back to blocking index creation on error
69
+
70
+ https://dev.mysql.com/doc/refman/8.4/en/create-index.html#:~:text=engine%20is%20changed.-,Table%20Copying%20and%20Locking%20Options,-ALGORITHM%20and%20LOCK
71
+ */
72
+ return this.knex
73
+ .raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) ALGORITHM=INPLACE LOCK=NONE`, [
74
+ constraintName,
75
+ collection,
76
+ field,
77
+ ])
78
+ .catch(() => blockingQuery);
79
+ }
80
+ return blockingQuery;
81
+ }
57
82
  }
@@ -2,7 +2,7 @@ import type { KNEX_TYPES } from '@directus/constants';
2
2
  import type { Column } from '@directus/schema';
3
3
  import type { Field, RawField, Relation, Type } from '@directus/types';
4
4
  import type { Knex } from 'knex';
5
- import type { Options, SortRecord, Sql } from '../types.js';
5
+ import type { CreateIndexOptions, Options, SortRecord, Sql } from '../types.js';
6
6
  import { SchemaHelper } from '../types.js';
7
7
  export declare class SchemaHelperOracle extends SchemaHelper {
8
8
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
@@ -20,4 +20,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
20
20
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
21
21
  getColumnNameMaxLength(): number;
22
22
  getTableNameMaxLength(): number;
23
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
23
24
  }
@@ -102,4 +102,17 @@ export class SchemaHelperOracle extends SchemaHelper {
102
102
  getTableNameMaxLength() {
103
103
  return 128;
104
104
  }
105
+ async createIndex(collection, field, options = {}) {
106
+ const isUnique = Boolean(options.unique);
107
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
108
+ if (options.attemptConcurrentIndex) {
109
+ // https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-INDEX.html#GUID-1F89BBC0-825F-4215-AF71-7588E31D8BFE__GUID-041E5429-065B-43D5-AC7F-66810140842C
110
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) ONLINE`, [
111
+ constraintName,
112
+ collection,
113
+ field,
114
+ ]);
115
+ }
116
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
117
+ }
105
118
  }
@@ -1,7 +1,8 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  getDatabaseSize(): Promise<number | null>;
6
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
7
8
  }
@@ -38,4 +38,17 @@ export class SchemaHelperPostgres extends SchemaHelper {
38
38
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
39
39
  }
40
40
  }
41
+ async createIndex(collection, field, options = {}) {
42
+ const isUnique = Boolean(options.unique);
43
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
44
+ // https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY
45
+ if (options.attemptConcurrentIndex) {
46
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX CONCURRENTLY ?? ON ?? (??)`, [
47
+ constraintName,
48
+ collection,
49
+ field,
50
+ ]);
51
+ }
52
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
53
+ }
41
54
  }
@@ -16,6 +16,10 @@ export type SortRecord = {
16
16
  alias: string;
17
17
  column: Knex.Raw;
18
18
  };
19
+ export type CreateIndexOptions = {
20
+ attemptConcurrentIndex?: boolean;
21
+ unique?: boolean;
22
+ };
19
23
  export declare abstract class SchemaHelper extends DatabaseHelper {
20
24
  isOneOfClients(clients: DatabaseClient[]): boolean;
21
25
  changeNullable(table: string, column: string, nullable: boolean): Promise<void>;
@@ -41,4 +45,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
41
45
  addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
42
46
  getColumnNameMaxLength(): number;
43
47
  getTableNameMaxLength(): number;
48
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
44
49
  }
@@ -118,4 +118,10 @@ export class SchemaHelper extends DatabaseHelper {
118
118
  getTableNameMaxLength() {
119
119
  return 64;
120
120
  }
121
+ async createIndex(collection, field, options = {}) {
122
+ // fall back to concurrent index creation
123
+ const isUnique = Boolean(options.unique);
124
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
125
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
126
+ }
121
127
  }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_fields', (table) => {
3
+ table.boolean('searchable').defaultTo(true).notNullable();
4
+ });
5
+ }
6
+ export async function down(knex) {
7
+ await knex.schema.alterTable('directus_fields', (table) => {
8
+ table.dropColumn('searchable');
9
+ });
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { SettingsService } from '../../services/settings.js';
4
+ import { getSchema } from '../../utils/get-schema.js';
5
+ import { email } from 'zod';
6
+ export async function up(knex) {
7
+ await knex.schema.alterTable('directus_settings', (table) => {
8
+ table.string('project_owner');
9
+ table.string('project_usage');
10
+ table.string('org_name');
11
+ table.boolean('product_updates');
12
+ table.string('project_status');
13
+ table.dropColumn('accepted_terms');
14
+ });
15
+ const env = useEnv();
16
+ const settingsService = new SettingsService({ schema: await getSchema() });
17
+ if (email().safeParse(env['PROJECT_OWNER']).success) {
18
+ await settingsService.setOwner({
19
+ project_owner: env['PROJECT_OWNER'],
20
+ org_name: null,
21
+ project_usage: null,
22
+ product_updates: false,
23
+ });
24
+ }
25
+ }
26
+ export async function down(knex) {
27
+ const env = useEnv();
28
+ const acceptedTerms = toBoolean(env['ACCEPT_TERMS']);
29
+ await knex.schema.alterTable('directus_settings', (table) => {
30
+ table.dropColumn('project_owner');
31
+ table.dropColumn('project_usage');
32
+ table.dropColumn('org_name');
33
+ table.dropColumn('product_updates');
34
+ table.dropColumn('project_status');
35
+ table.boolean('accepted_terms').defaultTo(acceptedTerms);
36
+ });
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { FieldsService } from '../../services/fields.js';
2
+ import { getSchema } from '../../utils/get-schema.js';
3
+ import { transaction } from '../../utils/transaction.js';
4
+ import { getHelpers } from '../helpers/index.js';
5
+ import { getDatabaseClient } from '../index.js';
6
+ const RETENTION_INDEXES = [
7
+ { collection: 'directus_activity', field: 'timestamp', ignore: [] },
8
+ // MySQL is ignored because it already has an index on revisions.parent
9
+ { collection: 'directus_revisions', field: 'parent', ignore: ['mysql'] },
10
+ ];
11
+ export async function up(knex) {
12
+ const client = getDatabaseClient(knex);
13
+ const helpers = getHelpers(knex);
14
+ const schema = await getSchema();
15
+ const service = new FieldsService({ knex, schema });
16
+ for (const { collection, field, ignore } of RETENTION_INDEXES) {
17
+ if (ignore.includes(client))
18
+ continue;
19
+ const existingColumn = await service.columnInfo(collection, field);
20
+ if (!existingColumn.is_indexed) {
21
+ await helpers.schema.createIndex(collection, field, { attemptConcurrentIndex: true });
22
+ }
23
+ }
24
+ }
25
+ export async function down(knex) {
26
+ const client = getDatabaseClient(knex);
27
+ const helpers = getHelpers(knex);
28
+ const schema = await getSchema();
29
+ const service = new FieldsService({ knex, schema });
30
+ for (const { collection, field, ignore } of RETENTION_INDEXES) {
31
+ if (ignore.includes(client))
32
+ continue;
33
+ const existingColumn = await service.columnInfo(collection, field);
34
+ if (existingColumn.is_indexed) {
35
+ await transaction(knex, async (trx) => {
36
+ await trx.schema.alterTable(collection, async (table) => {
37
+ table.dropIndex([field], helpers.schema.generateIndexName('index', collection, field));
38
+ });
39
+ });
40
+ }
41
+ }
42
+ }
@@ -2,7 +2,7 @@ import { InvalidQueryError } from '@directus/errors';
2
2
  import { clone } from 'lodash-es';
3
3
  import { getRelationInfo } from '../../../../utils/get-relation-info.js';
4
4
  import { getHelpers } from '../../../helpers/index.js';
5
- import { generateAlias } from './index.js';
5
+ import { generateJoinAlias } from '../../utils/generate-alias.js';
6
6
  export function addJoin({ path, collection, aliasMap, rootQuery, schema, knex }) {
7
7
  let hasMultiRelational = false;
8
8
  let isJoinAdded = false;
@@ -22,7 +22,7 @@ export function addJoin({ path, collection, aliasMap, rootQuery, schema, knex })
22
22
  ? aliasMap[`${parentFields}.${pathParts[0]}`]?.alias
23
23
  : aliasMap[pathParts[0]]?.alias;
24
24
  if (!existingAlias) {
25
- const alias = generateAlias();
25
+ const alias = generateJoinAlias(parentCollection, pathParts, relationType, parentFields);
26
26
  const aliasKey = parentFields ? `${parentFields}.${pathParts[0]}` : pathParts[0];
27
27
  const aliasedParentCollection = aliasMap[parentFields ?? '']?.alias || parentCollection;
28
28
  aliasMap[aliasKey] = { alias, collection: '' };
@@ -1,8 +1,8 @@
1
1
  import type { FieldOverview } from '@directus/types';
2
2
  export declare function getFilterType(fields: Record<string, FieldOverview>, key: string, collection?: string): {
3
- type: "string" | "boolean" | "json" | "text" | "integer" | "float" | "alias" | "uuid" | "binary" | "time" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
3
+ type: "string" | "boolean" | "binary" | "time" | "text" | "integer" | "float" | "alias" | "uuid" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "json" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
4
4
  special?: never;
5
5
  } | {
6
- type: "string" | "boolean" | "json" | "text" | "integer" | "float" | "alias" | "uuid" | "binary" | "time" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
6
+ type: "string" | "boolean" | "binary" | "time" | "text" | "integer" | "float" | "alias" | "uuid" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "json" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
7
7
  special: string[];
8
8
  };
@@ -1,7 +1,6 @@
1
1
  import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { AliasMap } from '../../../../utils/get-column-path.js';
4
- export declare const generateAlias: (size?: number) => string;
5
4
  type ApplyQueryOptions = {
6
5
  aliasMap?: AliasMap;
7
6
  isInnerQuery?: boolean;
@@ -1,14 +1,12 @@
1
- import { customAlphabet } from 'nanoid/non-secure';
2
1
  import { getHelpers } from '../../../helpers/index.js';
3
2
  import { applyCaseWhen } from '../../utils/apply-case-when.js';
4
3
  import { getColumn } from '../../utils/get-column.js';
5
- import { applyLimit, applyOffset } from './pagination.js';
6
- import { joinFilterWithCases } from './join-filter-with-cases.js';
7
- import { applySort } from './sort.js';
4
+ import { applyAggregate } from './aggregate.js';
8
5
  import { applyFilter } from './filter/index.js';
6
+ import { joinFilterWithCases } from './join-filter-with-cases.js';
7
+ import { applyLimit, applyOffset } from './pagination.js';
9
8
  import { applySearch } from './search.js';
10
- import { applyAggregate } from './aggregate.js';
11
- export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
9
+ import { applySort } from './sort.js';
12
10
  /**
13
11
  * Apply the Query to a given Knex query builder instance
14
12
  */
@@ -9,6 +9,8 @@ export function applySearch(knex, schema, dbQuery, searchQuery, collection, alia
9
9
  const { number: numberHelper } = getHelpers(knex);
10
10
  const allowedFields = new Set(permissions.filter((p) => p.collection === collection).flatMap((p) => p.fields ?? []));
11
11
  let fields = Object.entries(schema.collections[collection].fields);
12
+ // filter out fields that are not searchable
13
+ fields = fields.filter(([_name, field]) => field.searchable !== false && field.special.includes('conceal') !== true);
12
14
  const { cases, caseMap } = getCases(collection, permissions, []);
13
15
  // Add field restrictions if non-admin and "everything" is not allowed
14
16
  if (cases.length !== 0 && !allowedFields.has('*')) {
@@ -1,16 +1,17 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { cloneDeep } from 'lodash-es';
3
- import { applySort } from './apply-query/sort.js';
4
3
  import { getCollectionFromAlias } from '../../../utils/get-collection-from-alias.js';
5
- import { getColumn } from '../utils/get-column.js';
6
4
  import { getHelpers } from '../../helpers/index.js';
7
5
  import { applyCaseWhen } from '../utils/apply-case-when.js';
6
+ import { generateQueryAlias } from '../utils/generate-alias.js';
8
7
  import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
8
+ import { getColumn } from '../utils/get-column.js';
9
9
  import { getNodeAlias } from '../utils/get-field-alias.js';
10
10
  import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
11
11
  import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
12
- import applyQuery, { generateAlias } from './apply-query/index.js';
12
+ import applyQuery from './apply-query/index.js';
13
13
  import { applyLimit } from './apply-query/pagination.js';
14
+ import { applySort } from './apply-query/sort.js';
14
15
  export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }, { knex, schema }) {
15
16
  const aliasMap = Object.create(null);
16
17
  const env = useEnv();
@@ -95,11 +96,11 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
95
96
  if (needsInnerQuery) {
96
97
  let orderByString = '';
97
98
  const orderByFields = [];
98
- sortRecords.map((sortRecord) => {
99
+ sortRecords.map((sortRecord, index) => {
99
100
  if (orderByString.length !== 0) {
100
101
  orderByString += ', ';
101
102
  }
102
- const sortAlias = `sort_${generateAlias()}`;
103
+ const sortAlias = generateQueryAlias(table, queryCopy, `sort_${index}_${sortRecord.column}_${sortRecord.order}`);
103
104
  let orderByColumn;
104
105
  if (sortRecord.column.includes('.')) {
105
106
  const [alias, field] = sortRecord.column.split('.');
@@ -146,7 +147,7 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
146
147
  }
147
148
  if (!needsInnerQuery)
148
149
  return dbQuery;
149
- const innerCaseWhenAliasPrefix = generateAlias();
150
+ const innerCaseWhenAliasPrefix = generateQueryAlias(table, queryCopy, 'inner_case_when');
150
151
  if (hasCaseWhen) {
151
152
  /* If there are cases, we need to employ a trick in order to evaluate the case/when structure in the inner query,
152
153
  while passing the result of the evaluation to the outer query. The case/when needs to be evaluated in the inner
@@ -0,0 +1,6 @@
1
+ import type { Query } from '@directus/types';
2
+ import type { FnHelperOptions } from '../../helpers/fn/types.js';
3
+ export declare function generateAlias(context?: string): string;
4
+ export declare function generateQueryAlias(table: string, query: Query, path?: string): string;
5
+ export declare function generateRelationalQueryAlias(table: string, column: string, collectionName: string, options?: FnHelperOptions): string;
6
+ export declare function generateJoinAlias(collection: string, path: string[], relationType: string | null, parentFields?: string): string;
@@ -0,0 +1,57 @@
1
+ import { getSimpleHash } from '@directus/utils';
2
+ import { customAlphabet } from 'nanoid/non-secure';
3
+ // Fallback to original random alias generator
4
+ const generateRandomAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
5
+ // Context-aware alias generator
6
+ export function generateAlias(context = '') {
7
+ if (context) {
8
+ return generateDeterministicAlias(context);
9
+ }
10
+ return generateRandomAlias();
11
+ }
12
+ // Create a deterministic alias for general query contexts
13
+ export function generateQueryAlias(table, query, path = '') {
14
+ const context = JSON.stringify({
15
+ table,
16
+ path,
17
+ sort: query.sort,
18
+ group: query.group,
19
+ aggregate: query.aggregate,
20
+ // Exclude: limit, offset, page, search, filter - these are execution parameters
21
+ // that don't affect the underlying query structure requiring aliases
22
+ });
23
+ return generateDeterministicAlias(context);
24
+ }
25
+ // Create a deterministic context for relational count aliases
26
+ export function generateRelationalQueryAlias(table, column, collectionName, options) {
27
+ const context = JSON.stringify({
28
+ table,
29
+ column,
30
+ collectionName,
31
+ filter: options?.relationalCountOptions?.query?.filter,
32
+ });
33
+ return generateDeterministicAlias(context);
34
+ }
35
+ // Create a deterministic alias for join contexts
36
+ export function generateJoinAlias(collection, path, relationType, parentFields = '') {
37
+ const context = JSON.stringify({
38
+ collection,
39
+ path: path.join('.'),
40
+ relationType,
41
+ parentFields,
42
+ });
43
+ return generateDeterministicAlias(context);
44
+ }
45
+ // Generate deterministic alias based on context
46
+ function generateDeterministicAlias(context = '') {
47
+ const hash = getSimpleHash(context);
48
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz';
49
+ let result = '';
50
+ let num = parseInt(hash, 16);
51
+ // Generate 5 character alias
52
+ for (let i = 0; i < 5; i++) {
53
+ result += alphabet[num % alphabet.length];
54
+ num = Math.floor(num / alphabet.length);
55
+ }
56
+ return result;
57
+ }
package/dist/flows.js CHANGED
@@ -291,6 +291,7 @@ class FlowManager {
291
291
  [ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null,
292
292
  [ENV_KEY]: this.envs,
293
293
  };
294
+ context['flow'] ??= flow;
294
295
  let nextOperation = flow.operation;
295
296
  let lastOperationStatus = 'unknown';
296
297
  const steps = [];