@directus/api 30.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 +7 -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 +28 -11
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +28 -11
- 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/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- package/dist/controllers/users.js +17 -7
- package/dist/controllers/versions.js +3 -2
- package/dist/database/errors/dialects/mssql.d.ts +1 -1
- package/dist/database/errors/dialects/mssql.js +18 -10
- 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/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -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/README.md +46 -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/define.d.ts +2 -0
- package/dist/mcp/define.js +3 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/schema.d.ts +485 -0
- package/dist/mcp/schema.js +219 -0
- package/dist/mcp/server.d.ts +103 -0
- package/dist/mcp/server.js +310 -0
- package/dist/mcp/tools/assets.d.ts +3 -0
- package/dist/mcp/tools/assets.js +54 -0
- package/dist/mcp/tools/collections.d.ts +84 -0
- package/dist/mcp/tools/collections.js +90 -0
- package/dist/mcp/tools/fields.d.ts +101 -0
- package/dist/mcp/tools/fields.js +157 -0
- package/dist/mcp/tools/files.d.ts +235 -0
- package/dist/mcp/tools/files.js +103 -0
- package/dist/mcp/tools/flows.d.ts +323 -0
- package/dist/mcp/tools/flows.js +85 -0
- package/dist/mcp/tools/folders.d.ts +95 -0
- package/dist/mcp/tools/folders.js +96 -0
- package/dist/mcp/tools/index.d.ts +15 -0
- package/dist/mcp/tools/index.js +29 -0
- package/dist/mcp/tools/items.d.ts +87 -0
- package/dist/mcp/tools/items.js +141 -0
- package/dist/mcp/tools/operations.d.ts +171 -0
- package/dist/mcp/tools/operations.js +77 -0
- package/dist/mcp/tools/prompts/assets.md +8 -0
- package/dist/mcp/tools/prompts/collections.md +336 -0
- package/dist/mcp/tools/prompts/fields.md +521 -0
- package/dist/mcp/tools/prompts/files.md +180 -0
- package/dist/mcp/tools/prompts/flows.md +495 -0
- package/dist/mcp/tools/prompts/folders.md +34 -0
- package/dist/mcp/tools/prompts/index.d.ts +16 -0
- package/dist/mcp/tools/prompts/index.js +19 -0
- package/dist/mcp/tools/prompts/items.md +317 -0
- package/dist/mcp/tools/prompts/operations.md +721 -0
- package/dist/mcp/tools/prompts/relations.md +386 -0
- package/dist/mcp/tools/prompts/schema.md +130 -0
- package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
- package/dist/mcp/tools/prompts/system-prompt.md +44 -0
- package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
- package/dist/mcp/tools/relations.d.ts +73 -0
- package/dist/mcp/tools/relations.js +93 -0
- package/dist/mcp/tools/schema.d.ts +54 -0
- package/dist/mcp/tools/schema.js +317 -0
- package/dist/mcp/tools/system.d.ts +3 -0
- package/dist/mcp/tools/system.js +22 -0
- package/dist/mcp/tools/trigger-flow.d.ts +8 -0
- package/dist/mcp/tools/trigger-flow.js +48 -0
- package/dist/mcp/transport.d.ts +13 -0
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/types.d.ts +56 -0
- package/dist/mcp/types.js +1 -0
- 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/authentication.js +36 -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 +109 -32
- 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 +50 -24
- 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/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +15 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/tus/server.js +14 -9
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- 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 +2 -3
- 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/utils/versioning/deep-map-with-schema.d.ts +23 -0
- package/dist/utils/versioning/deep-map-with-schema.js +81 -0
- package/dist/utils/versioning/handle-version.d.ts +2 -2
- package/dist/utils/versioning/handle-version.js +47 -43
- package/dist/utils/versioning/split-recursive.d.ts +4 -0
- package/dist/utils/versioning/split-recursive.js +27 -0
- package/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +65 -66
- 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
|
@@ -11,7 +11,7 @@ import { isNestedMetaUpdate } from '../../../utils/apply-diff.js';
|
|
|
11
11
|
import { applySnapshot } from '../../../utils/apply-snapshot.js';
|
|
12
12
|
import { getSnapshotDiff } from '../../../utils/get-snapshot-diff.js';
|
|
13
13
|
import { getSnapshot } from '../../../utils/get-snapshot.js';
|
|
14
|
-
function filterSnapshotDiff(snapshot, filters) {
|
|
14
|
+
export function filterSnapshotDiff(snapshot, filters) {
|
|
15
15
|
const filterSet = new Set(filters);
|
|
16
16
|
function shouldKeep(item) {
|
|
17
17
|
if (filterSet.has(item.collection))
|
|
@@ -23,6 +23,7 @@ function filterSnapshotDiff(snapshot, filters) {
|
|
|
23
23
|
const filteredDiff = {
|
|
24
24
|
collections: snapshot.collections.filter((item) => shouldKeep(item)),
|
|
25
25
|
fields: snapshot.fields.filter((item) => shouldKeep(item)),
|
|
26
|
+
systemFields: snapshot.systemFields.filter((item) => shouldKeep(item)),
|
|
26
27
|
relations: snapshot.relations.filter((item) => shouldKeep(item)),
|
|
27
28
|
};
|
|
28
29
|
return filteredDiff;
|
|
@@ -53,6 +54,7 @@ export async function apply(snapshotPath, options) {
|
|
|
53
54
|
}
|
|
54
55
|
if (snapshotDiff.collections.length === 0 &&
|
|
55
56
|
snapshotDiff.fields.length === 0 &&
|
|
57
|
+
snapshotDiff.systemFields.length === 0 &&
|
|
56
58
|
snapshotDiff.relations.length === 0) {
|
|
57
59
|
logger.info('No changes to apply.');
|
|
58
60
|
database.destroy();
|
|
@@ -116,6 +118,27 @@ export async function apply(snapshotPath, options) {
|
|
|
116
118
|
}
|
|
117
119
|
sections.push(lines.join('\n'));
|
|
118
120
|
}
|
|
121
|
+
if (snapshotDiff.systemFields.length > 0) {
|
|
122
|
+
const lines = [chalk.underline.bold('System Fields:')];
|
|
123
|
+
for (const { collection, field, diff } of snapshotDiff.systemFields) {
|
|
124
|
+
if (diff[0]?.kind === DiffKind.EDIT) {
|
|
125
|
+
lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
|
|
126
|
+
for (const change of diff) {
|
|
127
|
+
const path = formatPath(change.path);
|
|
128
|
+
if (change.kind === DiffKind.EDIT) {
|
|
129
|
+
lines.push(` - Set ${path} to ${change.rhs}`);
|
|
130
|
+
}
|
|
131
|
+
else if (change.kind === DiffKind.DELETE) {
|
|
132
|
+
lines.push(` - Remove ${path}`);
|
|
133
|
+
}
|
|
134
|
+
else if (change.kind === DiffKind.NEW) {
|
|
135
|
+
lines.push(` - Add ${path} and set it to ${change.rhs}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
sections.push(lines.join('\n'));
|
|
141
|
+
}
|
|
119
142
|
if (snapshotDiff.relations.length > 0) {
|
|
120
143
|
const lines = [chalk.underline.bold('Relations:')];
|
|
121
144
|
for (const { collection, field, related_collection, diff } of snapshotDiff.relations) {
|
|
@@ -169,13 +192,13 @@ export async function apply(snapshotPath, options) {
|
|
|
169
192
|
process.exit(1);
|
|
170
193
|
}
|
|
171
194
|
}
|
|
172
|
-
function formatPath(path) {
|
|
195
|
+
export function formatPath(path) {
|
|
173
196
|
if (path.length === 1) {
|
|
174
197
|
return path.toString();
|
|
175
198
|
}
|
|
176
199
|
return path.slice(1).join('.');
|
|
177
200
|
}
|
|
178
|
-
function formatRelatedCollection(relatedCollection) {
|
|
201
|
+
export function formatRelatedCollection(relatedCollection) {
|
|
179
202
|
// Related collection doesn't exist for a2o relationship types
|
|
180
203
|
if (relatedCollection) {
|
|
181
204
|
return ` → ${relatedCollection}`;
|
|
@@ -11,13 +11,18 @@ router.post('/', asyncHandler(async (req, res, next) => {
|
|
|
11
11
|
accountability: req.accountability,
|
|
12
12
|
schema: req.schema,
|
|
13
13
|
});
|
|
14
|
+
const attemptConcurrentIndex = 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false';
|
|
14
15
|
if (Array.isArray(req.body)) {
|
|
15
|
-
const collectionKey = await collectionsService.createMany(req.body
|
|
16
|
+
const collectionKey = await collectionsService.createMany(req.body, {
|
|
17
|
+
attemptConcurrentIndex,
|
|
18
|
+
});
|
|
16
19
|
const records = await collectionsService.readMany(collectionKey);
|
|
17
20
|
res.locals['payload'] = { data: records || null };
|
|
18
21
|
}
|
|
19
22
|
else {
|
|
20
|
-
const collectionKey = await collectionsService.createOne(req.body
|
|
23
|
+
const collectionKey = await collectionsService.createOne(req.body, {
|
|
24
|
+
attemptConcurrentIndex,
|
|
25
|
+
});
|
|
21
26
|
const record = await collectionsService.readOne(collectionKey);
|
|
22
27
|
res.locals['payload'] = { data: record || null };
|
|
23
28
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TYPES } from '@directus/constants';
|
|
2
|
-
import { isDirectusError } from '@directus/errors';
|
|
2
|
+
import { ForbiddenError, isDirectusError } from '@directus/errors';
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
import Joi from 'joi';
|
|
5
5
|
import { ALIAS_TYPES } from '../constants.js';
|
|
@@ -7,8 +7,9 @@ import { ErrorCode, InvalidPayloadError } from '@directus/errors';
|
|
|
7
7
|
import validateCollection from '../middleware/collection-exists.js';
|
|
8
8
|
import { respond } from '../middleware/respond.js';
|
|
9
9
|
import useCollection from '../middleware/use-collection.js';
|
|
10
|
-
import { FieldsService } from '../services/fields.js';
|
|
10
|
+
import { FieldsService, systemFieldUpdateSchema } from '../services/fields.js';
|
|
11
11
|
import asyncHandler from '../utils/async-handler.js';
|
|
12
|
+
import { isSystemField } from '@directus/system-data';
|
|
12
13
|
const router = Router();
|
|
13
14
|
router.use(useCollection('directus_fields'));
|
|
14
15
|
router.get('/', asyncHandler(async (req, res, next) => {
|
|
@@ -64,7 +65,9 @@ router.post('/:collection', validateCollection, asyncHandler(async (req, res, ne
|
|
|
64
65
|
throw new InvalidPayloadError({ reason: error.message });
|
|
65
66
|
}
|
|
66
67
|
const field = req.body;
|
|
67
|
-
await service.createField(req.params['collection'], field
|
|
68
|
+
await service.createField(req.params['collection'], field, undefined, {
|
|
69
|
+
attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
|
|
70
|
+
});
|
|
68
71
|
try {
|
|
69
72
|
const createdField = await service.readOne(req.params['collection'], field.field);
|
|
70
73
|
res.locals['payload'] = { data: createdField || null };
|
|
@@ -85,7 +88,16 @@ router.patch('/:collection', validateCollection, asyncHandler(async (req, res, n
|
|
|
85
88
|
if (Array.isArray(req.body) === false) {
|
|
86
89
|
throw new InvalidPayloadError({ reason: 'Submitted body has to be an array' });
|
|
87
90
|
}
|
|
88
|
-
|
|
91
|
+
for (const fieldData of req.body) {
|
|
92
|
+
if (isSystemField(req.params['collection'], fieldData['field'])) {
|
|
93
|
+
const { error } = systemFieldUpdateSchema.safeParse(fieldData);
|
|
94
|
+
if (error)
|
|
95
|
+
throw error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await service.updateFields(req.params['collection'], req.body, {
|
|
99
|
+
attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
|
|
100
|
+
});
|
|
89
101
|
try {
|
|
90
102
|
const results = [];
|
|
91
103
|
for (const field of req.body) {
|
|
@@ -120,14 +132,22 @@ router.patch('/:collection/:field', validateCollection, asyncHandler(async (req,
|
|
|
120
132
|
accountability: req.accountability,
|
|
121
133
|
schema: req.schema,
|
|
122
134
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
if (isSystemField(req.params['collection'], req.params['field'])) {
|
|
136
|
+
const { error } = systemFieldUpdateSchema.safeParse(req.body);
|
|
137
|
+
if (error)
|
|
138
|
+
throw error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const { error } = updateSchema.validate(req.body);
|
|
142
|
+
if (error)
|
|
143
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
126
144
|
}
|
|
127
145
|
const fieldData = req.body;
|
|
128
146
|
if (!fieldData.field)
|
|
129
147
|
fieldData.field = req.params['field'];
|
|
130
|
-
await service.updateField(req.params['collection'], fieldData
|
|
148
|
+
await service.updateField(req.params['collection'], fieldData, {
|
|
149
|
+
attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
|
|
150
|
+
});
|
|
131
151
|
try {
|
|
132
152
|
const updatedField = await service.readOne(req.params['collection'], req.params['field']);
|
|
133
153
|
res.locals['payload'] = { data: updatedField || null };
|
|
@@ -145,6 +165,9 @@ router.delete('/:collection/:field', validateCollection, asyncHandler(async (req
|
|
|
145
165
|
accountability: req.accountability,
|
|
146
166
|
schema: req.schema,
|
|
147
167
|
});
|
|
168
|
+
if (isSystemField(req.params['collection'], req.params['field'])) {
|
|
169
|
+
throw new ForbiddenError();
|
|
170
|
+
}
|
|
148
171
|
await service.deleteField(req.params['collection'], req.params['field']);
|
|
149
172
|
return next();
|
|
150
173
|
}), respond);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ForbiddenError } from '@directus/errors';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { DirectusMCP } from '../mcp/index.js';
|
|
4
|
+
import { SettingsService } from '../services/settings.js';
|
|
5
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
6
|
+
const router = Router();
|
|
7
|
+
const mcpHandler = asyncHandler(async (req, res) => {
|
|
8
|
+
const settings = new SettingsService({
|
|
9
|
+
schema: req.schema,
|
|
10
|
+
});
|
|
11
|
+
const { mcp_enabled, mcp_allow_deletes, mcp_prompts_collection, mcp_system_prompt, mcp_system_prompt_enabled } = await settings.readSingleton({
|
|
12
|
+
fields: [
|
|
13
|
+
'mcp_enabled',
|
|
14
|
+
'mcp_allow_deletes',
|
|
15
|
+
'mcp_prompts_collection',
|
|
16
|
+
'mcp_system_prompt',
|
|
17
|
+
'mcp_system_prompt_enabled',
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
if (!mcp_enabled) {
|
|
21
|
+
throw new ForbiddenError({ reason: 'MCP must be enabled' });
|
|
22
|
+
}
|
|
23
|
+
const mcp = new DirectusMCP({
|
|
24
|
+
promptsCollection: mcp_prompts_collection,
|
|
25
|
+
allowDeletes: mcp_allow_deletes,
|
|
26
|
+
systemPromptEnabled: mcp_system_prompt_enabled,
|
|
27
|
+
systemPrompt: mcp_system_prompt,
|
|
28
|
+
});
|
|
29
|
+
mcp.handleRequest(req, res);
|
|
30
|
+
});
|
|
31
|
+
router.get('/', mcpHandler);
|
|
32
|
+
router.post('/', mcpHandler);
|
|
33
|
+
export default router;
|
|
@@ -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';
|
|
@@ -231,19 +233,27 @@ router.post('/me/tfa/generate/', asyncHandler(async (req, res, next) => {
|
|
|
231
233
|
if (!req.accountability?.user) {
|
|
232
234
|
throw new InvalidCredentialsError();
|
|
233
235
|
}
|
|
234
|
-
|
|
236
|
+
const currentUser = await getDatabase()
|
|
237
|
+
.select('provider')
|
|
238
|
+
.from('directus_users')
|
|
239
|
+
.where({ id: req.accountability.user })
|
|
240
|
+
.first();
|
|
241
|
+
const requiresPassword = currentUser?.['provider'] === DEFAULT_AUTH_PROVIDER;
|
|
242
|
+
if (requiresPassword && !req.body.password) {
|
|
235
243
|
throw new InvalidPayloadError({ reason: `"password" is required` });
|
|
236
244
|
}
|
|
237
245
|
const service = new TFAService({
|
|
238
246
|
accountability: req.accountability,
|
|
239
247
|
schema: req.schema,
|
|
240
248
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
if (requiresPassword) {
|
|
250
|
+
const authService = new AuthenticationService({
|
|
251
|
+
accountability: req.accountability,
|
|
252
|
+
schema: req.schema,
|
|
253
|
+
});
|
|
254
|
+
await authService.verifyPassword(req.accountability.user, req.body.password);
|
|
255
|
+
}
|
|
256
|
+
const { url, secret } = await service.generateTFA(req.accountability.user, requiresPassword);
|
|
247
257
|
res.locals['payload'] = { data: { secret, otpauth_url: url } };
|
|
248
258
|
return next();
|
|
249
259
|
}), respond);
|
|
@@ -154,9 +154,10 @@ router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
|
|
|
154
154
|
});
|
|
155
155
|
const version = await service.readOne(req.params['pk']);
|
|
156
156
|
const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);
|
|
157
|
-
const
|
|
157
|
+
const delta = version.delta ?? {};
|
|
158
|
+
delta[req.schema.collections[version.collection].primary] = version.item;
|
|
158
159
|
const main = await service.getMainItem(version['collection'], version['item']);
|
|
159
|
-
res.locals['payload'] = { data: { outdated, mainHash, current, main } };
|
|
160
|
+
res.locals['payload'] = { data: { outdated, mainHash, current: delta, main } };
|
|
160
161
|
return next();
|
|
161
162
|
}), respond);
|
|
162
163
|
router.post('/:pk/save', asyncHandler(async (req, res, next) => {
|
|
@@ -5,14 +5,15 @@ var MSSQLErrorCodes;
|
|
|
5
5
|
MSSQLErrorCodes[MSSQLErrorCodes["FOREIGN_KEY_VIOLATION"] = 547] = "FOREIGN_KEY_VIOLATION";
|
|
6
6
|
MSSQLErrorCodes[MSSQLErrorCodes["NOT_NULL_VIOLATION"] = 515] = "NOT_NULL_VIOLATION";
|
|
7
7
|
MSSQLErrorCodes[MSSQLErrorCodes["NUMERIC_VALUE_OUT_OF_RANGE"] = 220] = "NUMERIC_VALUE_OUT_OF_RANGE";
|
|
8
|
-
MSSQLErrorCodes[MSSQLErrorCodes["
|
|
8
|
+
MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION_INDEX"] = 2601] = "UNIQUE_VIOLATION_INDEX";
|
|
9
|
+
MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION_CONSTRAINT"] = 2627] = "UNIQUE_VIOLATION_CONSTRAINT";
|
|
9
10
|
MSSQLErrorCodes[MSSQLErrorCodes["VALUE_LIMIT_VIOLATION"] = 2628] = "VALUE_LIMIT_VIOLATION";
|
|
10
11
|
})(MSSQLErrorCodes || (MSSQLErrorCodes = {}));
|
|
11
12
|
export async function extractError(error, data) {
|
|
12
13
|
switch (error.number) {
|
|
13
|
-
case MSSQLErrorCodes.
|
|
14
|
-
case
|
|
15
|
-
return await uniqueViolation();
|
|
14
|
+
case MSSQLErrorCodes.UNIQUE_VIOLATION_CONSTRAINT:
|
|
15
|
+
case MSSQLErrorCodes.UNIQUE_VIOLATION_INDEX:
|
|
16
|
+
return await uniqueViolation(error);
|
|
16
17
|
case MSSQLErrorCodes.NUMERIC_VALUE_OUT_OF_RANGE:
|
|
17
18
|
return numericValueOutOfRange();
|
|
18
19
|
case MSSQLErrorCodes.VALUE_LIMIT_VIOLATION:
|
|
@@ -23,14 +24,20 @@ export async function extractError(error, data) {
|
|
|
23
24
|
return foreignKeyViolation();
|
|
24
25
|
}
|
|
25
26
|
return error;
|
|
26
|
-
async function uniqueViolation() {
|
|
27
|
+
async function uniqueViolation(error) {
|
|
27
28
|
/**
|
|
28
29
|
* NOTE:
|
|
29
|
-
* SQL Server doesn't return the name of the offending column when a unique
|
|
30
|
+
* SQL Server doesn't return the name of the offending column when a unique error is thrown:
|
|
30
31
|
*
|
|
32
|
+
* Constraint:
|
|
31
33
|
* insert into [articles] ([unique]) values (@p0)
|
|
32
|
-
* - Violation of UNIQUE KEY constraint '
|
|
33
|
-
*
|
|
34
|
+
* - Violation of UNIQUE KEY constraint 'unique_contraint_name'. Cannot insert duplicate key in object 'dbo.article'.
|
|
35
|
+
* The duplicate key value is (rijk).
|
|
36
|
+
*
|
|
37
|
+
* Index:
|
|
38
|
+
* insert into [articles] ([unique]) values (@p0)
|
|
39
|
+
* - Cannot insert duplicate key row in object 'dbo.articles' with unique index 'unique_index_name'.
|
|
40
|
+
* The duplicate key value is (rijk).
|
|
34
41
|
*
|
|
35
42
|
* While it's not ideal, the best next thing we can do is extract the column name from
|
|
36
43
|
* information_schema when this happens
|
|
@@ -41,8 +48,9 @@ export async function extractError(error, data) {
|
|
|
41
48
|
const parenMatches = error.message.match(betweenParens);
|
|
42
49
|
if (!quoteMatches || !parenMatches)
|
|
43
50
|
return error;
|
|
44
|
-
const
|
|
45
|
-
|
|
51
|
+
const [keyNameMatchIndex, collectionNameMatchIndex] = error.number === MSSQLErrorCodes.UNIQUE_VIOLATION_INDEX ? [1, 0] : [0, 1];
|
|
52
|
+
const keyName = quoteMatches[keyNameMatchIndex].slice(1, -1);
|
|
53
|
+
let collection = quoteMatches[collectionNameMatchIndex].slice(1, -1);
|
|
46
54
|
let field = null;
|
|
47
55
|
if (keyName) {
|
|
48
56
|
const database = getDatabase();
|
|
@@ -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
|
}
|