@directus/api 30.0.0 → 31.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 +5 -0
- package/dist/auth/drivers/oauth2.js +17 -3
- package/dist/auth/drivers/openid.js +17 -3
- package/dist/controllers/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- 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/migrations/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -0
- package/dist/database/run-ast/README.md +46 -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 +97 -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/services/authentication.js +36 -0
- package/dist/services/fields.js +4 -4
- package/dist/services/items.js +14 -4
- package/dist/services/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/server.js +1 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- package/dist/types/auth.d.ts +2 -1
- 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/package.json +30 -29
package/dist/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
|
|
3
3
|
import { handlePressure } from '@directus/pressure';
|
|
4
|
+
import { toBoolean } from '@directus/utils';
|
|
4
5
|
import cookieParser from 'cookie-parser';
|
|
5
6
|
import express from 'express';
|
|
6
7
|
import { merge } from 'lodash-es';
|
|
@@ -23,6 +24,7 @@ import flowsRouter from './controllers/flows.js';
|
|
|
23
24
|
import foldersRouter from './controllers/folders.js';
|
|
24
25
|
import graphqlRouter from './controllers/graphql.js';
|
|
25
26
|
import itemsRouter from './controllers/items.js';
|
|
27
|
+
import mcpRouter from './controllers/mcp.js';
|
|
26
28
|
import metricsRouter from './controllers/metrics.js';
|
|
27
29
|
import notFoundHandler from './controllers/not-found.js';
|
|
28
30
|
import notificationsRouter from './controllers/notifications.js';
|
|
@@ -225,6 +227,9 @@ export default async function createApp() {
|
|
|
225
227
|
app.use('/flows', flowsRouter);
|
|
226
228
|
app.use('/folders', foldersRouter);
|
|
227
229
|
app.use('/items', itemsRouter);
|
|
230
|
+
if (toBoolean(env['MCP_ENABLED']) === true) {
|
|
231
|
+
app.use('/mcp', mcpRouter);
|
|
232
|
+
}
|
|
228
233
|
if (env['METRICS_ENABLED'] === true) {
|
|
229
234
|
app.use('/metrics', metricsRouter);
|
|
230
235
|
}
|
|
@@ -273,10 +273,11 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
273
273
|
const codeVerifier = provider.generateCodeVerifier();
|
|
274
274
|
const prompt = !!req.query['prompt'];
|
|
275
275
|
const redirect = req.query['redirect'];
|
|
276
|
+
const otp = req.query['otp'];
|
|
276
277
|
if (isLoginRedirectAllowed(redirect, providerName) === false) {
|
|
277
278
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
278
279
|
}
|
|
279
|
-
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
|
|
280
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
|
|
280
281
|
expiresIn: '5m',
|
|
281
282
|
issuer: 'directus',
|
|
282
283
|
});
|
|
@@ -299,7 +300,8 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
299
300
|
logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
|
|
300
301
|
throw new InvalidCredentialsError();
|
|
301
302
|
}
|
|
302
|
-
const { verifier,
|
|
303
|
+
const { verifier, prompt, otp } = tokenData;
|
|
304
|
+
let { redirect } = tokenData;
|
|
303
305
|
const accountability = createDefaultAccountability({
|
|
304
306
|
ip: getIPFromReq(req),
|
|
305
307
|
});
|
|
@@ -321,7 +323,7 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
321
323
|
code: req.query['code'],
|
|
322
324
|
codeVerifier: verifier,
|
|
323
325
|
state: req.query['state'],
|
|
324
|
-
}, { session: authMode === 'session' });
|
|
326
|
+
}, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
|
|
325
327
|
}
|
|
326
328
|
catch (error) {
|
|
327
329
|
// Prompt user for a new refresh_token if invalidated
|
|
@@ -342,6 +344,18 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
342
344
|
throw error;
|
|
343
345
|
}
|
|
344
346
|
const { accessToken, refreshToken, expires } = authResponse;
|
|
347
|
+
try {
|
|
348
|
+
const claims = verifyJWT(accessToken, getSecret());
|
|
349
|
+
if (claims?.enforce_tfa === true) {
|
|
350
|
+
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
|
|
351
|
+
if (redirect)
|
|
352
|
+
url.setQuery('redirect', redirect);
|
|
353
|
+
redirect = url.toString();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
logger.warn(e, `[OAuth2] Unexpected error during OAuth2 login`);
|
|
358
|
+
}
|
|
345
359
|
if (redirect) {
|
|
346
360
|
if (authMode === 'session') {
|
|
347
361
|
res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
@@ -325,10 +325,11 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
325
325
|
const codeVerifier = provider.generateCodeVerifier();
|
|
326
326
|
const prompt = !!req.query['prompt'];
|
|
327
327
|
const redirect = req.query['redirect'];
|
|
328
|
+
const otp = req.query['otp'];
|
|
328
329
|
if (isLoginRedirectAllowed(redirect, providerName) === false) {
|
|
329
330
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
330
331
|
}
|
|
331
|
-
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
|
|
332
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
|
|
332
333
|
expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
|
|
333
334
|
issuer: 'directus',
|
|
334
335
|
});
|
|
@@ -361,7 +362,8 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
361
362
|
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
|
|
362
363
|
return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
|
|
363
364
|
}
|
|
364
|
-
const { verifier,
|
|
365
|
+
const { verifier, prompt, otp } = tokenData;
|
|
366
|
+
let { redirect } = tokenData;
|
|
365
367
|
const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
|
|
366
368
|
const userAgent = req.get('user-agent')?.substring(0, 1024);
|
|
367
369
|
if (userAgent)
|
|
@@ -382,7 +384,7 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
382
384
|
codeVerifier: verifier,
|
|
383
385
|
state: req.query['state'],
|
|
384
386
|
iss: req.query['iss'],
|
|
385
|
-
}, { session: authMode === 'session' });
|
|
387
|
+
}, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
|
|
386
388
|
}
|
|
387
389
|
catch (error) {
|
|
388
390
|
// Prompt user for a new refresh_token if invalidated
|
|
@@ -404,6 +406,18 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
404
406
|
throw error;
|
|
405
407
|
}
|
|
406
408
|
const { accessToken, refreshToken, expires } = authResponse;
|
|
409
|
+
try {
|
|
410
|
+
const claims = verifyJWT(accessToken, getSecret());
|
|
411
|
+
if (claims?.enforce_tfa === true) {
|
|
412
|
+
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
|
|
413
|
+
if (redirect)
|
|
414
|
+
url.setQuery('redirect', redirect);
|
|
415
|
+
redirect = url.toString();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
logger.warn(e, `[OpenID] Unexpected error during OpenID login`);
|
|
420
|
+
}
|
|
407
421
|
if (redirect) {
|
|
408
422
|
if (authMode === 'session') {
|
|
409
423
|
res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
@@ -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;
|
|
@@ -11,6 +11,8 @@ import { TFAService } from '../services/tfa.js';
|
|
|
11
11
|
import { UsersService } from '../services/users.js';
|
|
12
12
|
import asyncHandler from '../utils/async-handler.js';
|
|
13
13
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
14
|
+
import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
|
|
15
|
+
import { getDatabase } from '../database/index.js';
|
|
14
16
|
const router = express.Router();
|
|
15
17
|
router.use(useCollection('directus_users'));
|
|
16
18
|
router.post('/', asyncHandler(async (req, res, next) => {
|
|
@@ -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();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
3
|
+
table.boolean('mcp_enabled').defaultTo(false).notNullable();
|
|
4
|
+
table.boolean('mcp_allow_deletes').defaultTo(false).notNullable();
|
|
5
|
+
table.string('mcp_prompts_collection').defaultTo(null).nullable();
|
|
6
|
+
table.boolean('mcp_system_prompt_enabled').defaultTo(true).notNullable();
|
|
7
|
+
table.text('mcp_system_prompt').defaultTo(null).nullable();
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export async function down(knex) {
|
|
11
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
12
|
+
table.dropColumn('mcp_enabled');
|
|
13
|
+
table.dropColumn('mcp_allow_deletes');
|
|
14
|
+
table.dropColumn('mcp_prompts_collection');
|
|
15
|
+
table.dropColumn('mcp_system_prompt_enabled');
|
|
16
|
+
table.dropColumn('mcp_system_prompt');
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Internal RunAst Documentation
|
|
2
|
+
|
|
3
|
+
As the `runAst()` function is a crucial and complex part of Directus, this document intends to get you started by
|
|
4
|
+
providing a high level overview.
|
|
5
|
+
|
|
6
|
+
## AST
|
|
7
|
+
|
|
8
|
+
When a request is made through REST, GQL or WebSockets, we internally turn this request into an AST before this AST
|
|
9
|
+
get's translated into one or more SQL requests which are then send to the database.
|
|
10
|
+
|
|
11
|
+
On the root level of an AST is the root node which then references a list of child nodes. Each child node can be either
|
|
12
|
+
a primitive field (FieldNode), a relational field (M2ONode, O2MNode, A2MNode) or a function field (FunctionFieldNode).
|
|
13
|
+
Relational fields can also have additional children and nested queries.
|
|
14
|
+
|
|
15
|
+
### FieldNode
|
|
16
|
+
|
|
17
|
+
Describes a primitive field and is also a leaf node of the AST.
|
|
18
|
+
|
|
19
|
+
### Relational Fields
|
|
20
|
+
|
|
21
|
+
The M2ONode, O2MNode and A2MNode all will recursively call the `runAst()` function and the resulting data will then be
|
|
22
|
+
injected into the data of the level above.
|
|
23
|
+
|
|
24
|
+
### FunctionFieldNode
|
|
25
|
+
|
|
26
|
+
The only case where a function field node get's inserted into the ast, is for the count(o2m_relation) case, all other
|
|
27
|
+
functions are kept as FieldNode's with the name containing the function.
|
|
28
|
+
|
|
29
|
+
## RunAst()
|
|
30
|
+
|
|
31
|
+
The `runAst()` does translates the AST into an executable SQL request, requests the appropiate data from the database
|
|
32
|
+
and then returns a single or a list of items as the result.
|
|
33
|
+
|
|
34
|
+
### GetDBQuery()
|
|
35
|
+
|
|
36
|
+
The RootNode and relational fields have each their own query parameters. These parameters need to be applied to the SQL
|
|
37
|
+
query. This is done by the `getDBQuery()` function which internally uses `applyQuery()`.
|
|
38
|
+
|
|
39
|
+
<!-- TODO: Describe in larger detail what the GetDBQuery exactly does, i.e. why and when does it need an inner query, why does it sometimes call applyQuery() multiple times in different places -->
|
|
40
|
+
|
|
41
|
+
#### ApplyQuery()
|
|
42
|
+
|
|
43
|
+
`applyQuery()` takes the query and modifies the SQL request so that filters, sorts and all the other variants of query
|
|
44
|
+
parameters are applied.A special behaviour is filtering, sorting or other parameters on nested fields. To make this
|
|
45
|
+
possible, the `addJoin()` joins the required tables and then registers itself into the aliasMap so that the next time
|
|
46
|
+
the same collection should be joined, instead it will just reuse the already existing join.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './server.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './server.js';
|