@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.
- package/dist/app.js +2 -0
- package/dist/auth/auth.d.ts +2 -1
- package/dist/auth/auth.js +7 -2
- package/dist/auth/drivers/ldap.d.ts +0 -2
- package/dist/auth/drivers/ldap.js +9 -7
- package/dist/auth/drivers/oauth2.d.ts +0 -2
- package/dist/auth/drivers/oauth2.js +11 -8
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +11 -8
- package/dist/auth/drivers/saml.d.ts +0 -2
- package/dist/auth/drivers/saml.js +5 -5
- package/dist/auth.js +1 -2
- package/dist/cli/commands/bootstrap/index.js +12 -33
- package/dist/cli/commands/init/index.js +1 -1
- package/dist/cli/commands/schema/apply.d.ts +4 -0
- package/dist/cli/commands/schema/apply.js +26 -3
- package/dist/controllers/collections.js +7 -2
- package/dist/controllers/fields.js +31 -8
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- package/dist/controllers/users.js +2 -2
- package/dist/database/helpers/fn/types.js +3 -3
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +23 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +25 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +13 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +13 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
- package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
- package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
- package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
- package/dist/database/run-ast/lib/apply-query/index.js +4 -6
- package/dist/database/run-ast/lib/apply-query/search.js +2 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -6
- package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
- package/dist/database/run-ast/utils/generate-alias.js +57 -0
- package/dist/flows.js +1 -0
- package/dist/mcp/schema.d.ts +14 -14
- package/dist/mcp/schema.js +6 -6
- package/dist/mcp/server.d.ts +9 -3
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools/collections.d.ts +1 -1
- package/dist/mcp/tools/fields.d.ts +1 -1
- package/dist/mcp/tools/files.d.ts +25 -25
- package/dist/mcp/tools/flows.d.ts +36 -36
- package/dist/mcp/tools/folders.d.ts +18 -18
- package/dist/mcp/tools/items.d.ts +18 -18
- package/dist/mcp/tools/operations.d.ts +19 -19
- package/dist/mcp/tools/prompts/items.md +1 -1
- package/dist/metrics/lib/create-metrics.js +16 -25
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/operations/mail/index.js +3 -1
- package/dist/operations/mail/rate-limiter.d.ts +1 -0
- package/dist/operations/mail/rate-limiter.js +29 -0
- package/dist/permissions/modules/process-payload/process-payload.js +3 -10
- package/dist/permissions/modules/validate-access/validate-access.js +2 -3
- package/dist/schedules/metrics.js +6 -2
- package/dist/schedules/project.d.ts +4 -0
- package/dist/schedules/project.js +27 -0
- package/dist/services/collections.d.ts +3 -3
- package/dist/services/collections.js +16 -1
- package/dist/services/fields.d.ts +21 -5
- package/dist/services/fields.js +105 -28
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +49 -5
- package/dist/services/graphql/schema/parse-query.js +8 -8
- package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
- package/dist/services/graphql/utils/aggregate-query.js +5 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
- package/dist/services/import-export.d.ts +9 -1
- package/dist/services/import-export.js +287 -101
- package/dist/services/items.d.ts +1 -1
- package/dist/services/items.js +36 -20
- package/dist/services/mail/index.js +2 -0
- package/dist/services/mail/rate-limiter.d.ts +1 -0
- package/dist/services/mail/rate-limiter.js +29 -0
- package/dist/services/meta.js +28 -24
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +14 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tus/server.js +14 -9
- package/dist/telemetry/lib/get-report.js +4 -4
- package/dist/telemetry/lib/send-report.d.ts +6 -1
- package/dist/telemetry/lib/send-report.js +3 -1
- package/dist/telemetry/types/report.d.ts +17 -1
- package/dist/telemetry/utils/get-settings.d.ts +9 -0
- package/dist/telemetry/utils/get-settings.js +14 -0
- package/dist/test-utils/README.md +760 -0
- package/dist/test-utils/cache.d.ts +51 -0
- package/dist/test-utils/cache.js +59 -0
- package/dist/test-utils/database.d.ts +48 -0
- package/dist/test-utils/database.js +52 -0
- package/dist/test-utils/emitter.d.ts +35 -0
- package/dist/test-utils/emitter.js +38 -0
- package/dist/test-utils/fields-service.d.ts +28 -0
- package/dist/test-utils/fields-service.js +36 -0
- package/dist/test-utils/items-service.d.ts +23 -0
- package/dist/test-utils/items-service.js +37 -0
- package/dist/test-utils/knex.d.ts +164 -0
- package/dist/test-utils/knex.js +268 -0
- package/dist/test-utils/schema.d.ts +26 -0
- package/dist/test-utils/schema.js +35 -0
- package/dist/types/auth.d.ts +0 -2
- package/dist/utils/apply-diff.js +15 -0
- package/dist/utils/create-admin.d.ts +11 -0
- package/dist/utils/create-admin.js +50 -0
- package/dist/utils/get-schema.js +5 -3
- package/dist/utils/get-snapshot-diff.js +49 -5
- package/dist/utils/get-snapshot.js +13 -7
- package/dist/utils/sanitize-schema.d.ts +11 -4
- package/dist/utils/sanitize-schema.js +9 -6
- package/dist/utils/schedule.js +15 -19
- package/dist/utils/validate-diff.js +31 -0
- package/dist/utils/validate-snapshot.js +7 -0
- package/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +63 -65
- package/dist/cli/utils/defaults.d.ts +0 -4
- package/dist/cli/utils/defaults.js +0 -17
- package/dist/telemetry/utils/get-project-id.d.ts +0 -2
- 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 =
|
|
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,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,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,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 {
|
|
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 =
|
|
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" | "
|
|
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" | "
|
|
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 {
|
|
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 {
|
|
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
|
|
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_${
|
|
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 =
|
|
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