@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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CollectionsService } from '../../services/collections.js';
|
|
3
|
+
import { FieldsService } from '../../services/fields.js';
|
|
4
|
+
import { RelationsService } from '../../services/relations.js';
|
|
5
|
+
import { defineTool } from '../define.js';
|
|
6
|
+
import prompts from './prompts/index.js';
|
|
7
|
+
export const SchemaValidateSchema = z.strictObject({
|
|
8
|
+
keys: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
export const SchemaInputSchema = z.object({
|
|
11
|
+
keys: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Collection names to get detailed schema for. If omitted, returns a lightweight list of all collections.'),
|
|
15
|
+
});
|
|
16
|
+
export const schema = defineTool({
|
|
17
|
+
name: 'schema',
|
|
18
|
+
description: prompts.schema,
|
|
19
|
+
annotations: {
|
|
20
|
+
title: 'Directus - Schema',
|
|
21
|
+
},
|
|
22
|
+
inputSchema: SchemaInputSchema,
|
|
23
|
+
validateSchema: SchemaValidateSchema,
|
|
24
|
+
async handler({ args, accountability, schema }) {
|
|
25
|
+
const serviceOptions = {
|
|
26
|
+
schema,
|
|
27
|
+
accountability,
|
|
28
|
+
};
|
|
29
|
+
const collectionsService = new CollectionsService(serviceOptions);
|
|
30
|
+
const collections = await collectionsService.readByQuery();
|
|
31
|
+
// If no keys provided, return lightweight collection list
|
|
32
|
+
if (!args.keys || args.keys.length === 0) {
|
|
33
|
+
const lightweightOverview = {
|
|
34
|
+
collections: [],
|
|
35
|
+
collection_folders: [],
|
|
36
|
+
notes: {},
|
|
37
|
+
};
|
|
38
|
+
collections.forEach((collection) => {
|
|
39
|
+
// Separate folders from real collections
|
|
40
|
+
if (!collection.schema) {
|
|
41
|
+
lightweightOverview.collection_folders.push(collection.collection);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lightweightOverview.collections.push(collection.collection);
|
|
45
|
+
}
|
|
46
|
+
// Extract note if exists (for both collections and folders)
|
|
47
|
+
if (collection.meta?.note && !collection.meta.note.startsWith('$t')) {
|
|
48
|
+
lightweightOverview.notes[collection.collection] = collection.meta.note;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
type: 'text',
|
|
53
|
+
data: lightweightOverview,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// If keys provided, return detailed schema for requested collections
|
|
57
|
+
const overview = {};
|
|
58
|
+
const fieldsService = new FieldsService(serviceOptions);
|
|
59
|
+
const fields = await fieldsService.readAll();
|
|
60
|
+
const relationsService = new RelationsService(serviceOptions);
|
|
61
|
+
const relations = await relationsService.readAll();
|
|
62
|
+
const snapshot = {
|
|
63
|
+
collections,
|
|
64
|
+
fields,
|
|
65
|
+
relations,
|
|
66
|
+
};
|
|
67
|
+
fields.forEach((field) => {
|
|
68
|
+
// Skip collections not requested
|
|
69
|
+
if (!args.keys?.includes(field.collection))
|
|
70
|
+
return;
|
|
71
|
+
// Skip UI-only fields
|
|
72
|
+
if (field.type === 'alias' && field.meta?.special?.includes('no-data'))
|
|
73
|
+
return;
|
|
74
|
+
if (!overview[field.collection]) {
|
|
75
|
+
overview[field.collection] = {};
|
|
76
|
+
}
|
|
77
|
+
const fieldOverview = {
|
|
78
|
+
type: field.type,
|
|
79
|
+
};
|
|
80
|
+
if (field.schema?.is_primary_key) {
|
|
81
|
+
fieldOverview.primary_key = field.schema?.is_primary_key;
|
|
82
|
+
}
|
|
83
|
+
if (field.meta?.required) {
|
|
84
|
+
fieldOverview.required = field.meta.required;
|
|
85
|
+
}
|
|
86
|
+
if (field.meta?.readonly) {
|
|
87
|
+
fieldOverview.readonly = field.meta.readonly;
|
|
88
|
+
}
|
|
89
|
+
if (field.meta?.note) {
|
|
90
|
+
fieldOverview.note = field.meta.note;
|
|
91
|
+
}
|
|
92
|
+
if (field.meta?.interface) {
|
|
93
|
+
fieldOverview.interface = {
|
|
94
|
+
type: field.meta.interface,
|
|
95
|
+
};
|
|
96
|
+
if (field.meta.options?.['choices']) {
|
|
97
|
+
fieldOverview.interface.choices = field.meta.options['choices'].map(
|
|
98
|
+
// Only return the value of the choice to reduce size and potential for confusion.
|
|
99
|
+
(choice) => choice.value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Process nested fields for JSON fields with options.fields (like repeaters)
|
|
103
|
+
if (field.type === 'json' && field.meta?.options?.['fields']) {
|
|
104
|
+
const nestedFields = field.meta.options['fields'];
|
|
105
|
+
fieldOverview.fields = processNestedFields({
|
|
106
|
+
fields: nestedFields,
|
|
107
|
+
maxDepth: 5,
|
|
108
|
+
currentDepth: 0,
|
|
109
|
+
snapshot,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Handle collection-item-dropdown interface
|
|
113
|
+
if (field.type === 'json' && field.meta?.interface === 'collection-item-dropdown') {
|
|
114
|
+
fieldOverview.fields = processCollectionItemDropdown({
|
|
115
|
+
field,
|
|
116
|
+
snapshot,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Handle relationships
|
|
120
|
+
if (field.meta?.special) {
|
|
121
|
+
const relationshipType = getRelationType(field.meta.special);
|
|
122
|
+
if (relationshipType) {
|
|
123
|
+
fieldOverview.relation = buildRelationInfo(field, relationshipType, snapshot);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
overview[field.collection][field.field] = fieldOverview;
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
type: 'text',
|
|
130
|
+
data: overview,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Helpers
|
|
135
|
+
function processNestedFields(options) {
|
|
136
|
+
const { fields, maxDepth = 5, currentDepth = 0, snapshot } = options;
|
|
137
|
+
const result = {};
|
|
138
|
+
if (currentDepth >= maxDepth) {
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(fields)) {
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
const fieldKey = field.field || field.name;
|
|
146
|
+
if (!fieldKey)
|
|
147
|
+
continue;
|
|
148
|
+
const fieldOverview = {
|
|
149
|
+
type: field.type ?? 'any',
|
|
150
|
+
};
|
|
151
|
+
if (field.meta) {
|
|
152
|
+
const { required, readonly, note, interface: interfaceConfig, options } = field.meta;
|
|
153
|
+
if (required)
|
|
154
|
+
fieldOverview.required = required;
|
|
155
|
+
if (readonly)
|
|
156
|
+
fieldOverview.readonly = readonly;
|
|
157
|
+
if (note)
|
|
158
|
+
fieldOverview.note = note;
|
|
159
|
+
if (interfaceConfig) {
|
|
160
|
+
fieldOverview.interface = { type: interfaceConfig };
|
|
161
|
+
if (options?.choices) {
|
|
162
|
+
fieldOverview.interface.choices = options.choices;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Handle nested fields recursively
|
|
167
|
+
const nestedFields = field.meta?.options?.fields || field.options?.fields;
|
|
168
|
+
if (field.type === 'json' && nestedFields) {
|
|
169
|
+
fieldOverview.fields = processNestedFields({
|
|
170
|
+
fields: nestedFields,
|
|
171
|
+
maxDepth,
|
|
172
|
+
currentDepth: currentDepth + 1,
|
|
173
|
+
snapshot,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Handle collection-item-dropdown interface
|
|
177
|
+
if (field.type === 'json' && field.meta?.interface === 'collection-item-dropdown') {
|
|
178
|
+
fieldOverview.fields = processCollectionItemDropdown({
|
|
179
|
+
field,
|
|
180
|
+
snapshot,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
result[fieldKey] = fieldOverview;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function processCollectionItemDropdown(options) {
|
|
188
|
+
const { field, snapshot } = options;
|
|
189
|
+
const selectedCollection = field.meta?.options?.['selectedCollection'];
|
|
190
|
+
let keyType = 'string | number | uuid';
|
|
191
|
+
// Find the primary key type for the selected collection
|
|
192
|
+
if (selectedCollection && snapshot?.fields) {
|
|
193
|
+
const primaryKeyField = snapshot.fields.find((f) => f.collection === selectedCollection && f.schema?.is_primary_key);
|
|
194
|
+
if (primaryKeyField) {
|
|
195
|
+
keyType = primaryKeyField.type;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
collection: {
|
|
200
|
+
value: selectedCollection,
|
|
201
|
+
type: 'string',
|
|
202
|
+
},
|
|
203
|
+
key: {
|
|
204
|
+
type: keyType,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function getRelationType(special) {
|
|
209
|
+
if (special.includes('m2o') || special.includes('file'))
|
|
210
|
+
return 'm2o';
|
|
211
|
+
if (special.includes('o2m'))
|
|
212
|
+
return 'o2m';
|
|
213
|
+
if (special.includes('m2m') || special.includes('files'))
|
|
214
|
+
return 'm2m';
|
|
215
|
+
if (special.includes('m2a'))
|
|
216
|
+
return 'm2a';
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function buildRelationInfo(field, type, snapshot) {
|
|
220
|
+
switch (type) {
|
|
221
|
+
case 'm2o':
|
|
222
|
+
return buildManyToOneRelation(field, snapshot);
|
|
223
|
+
case 'o2m':
|
|
224
|
+
return buildOneToManyRelation(field, snapshot);
|
|
225
|
+
case 'm2m':
|
|
226
|
+
return buildManyToManyRelation(field, snapshot);
|
|
227
|
+
case 'm2a':
|
|
228
|
+
return buildManyToAnyRelation(field, snapshot);
|
|
229
|
+
default:
|
|
230
|
+
return { type };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function buildManyToOneRelation(field, snapshot) {
|
|
234
|
+
// For M2O, the relation is directly on this field
|
|
235
|
+
const relation = snapshot.relations.find((r) => r.collection === field.collection && r.field === field.field);
|
|
236
|
+
// The target collection is either in related_collection or foreign_key_table
|
|
237
|
+
const targetCollection = relation?.related_collection || relation?.schema?.foreign_key_table || field.schema?.foreign_key_table;
|
|
238
|
+
return {
|
|
239
|
+
type: 'm2o',
|
|
240
|
+
collection: targetCollection,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function buildOneToManyRelation(field, snapshot) {
|
|
244
|
+
// For O2M, we need to find the relation that points BACK to this field
|
|
245
|
+
// The relation will have this field stored in meta.one_field
|
|
246
|
+
const reverseRelation = snapshot.relations.find((r) => r.meta?.one_collection === field.collection && r.meta?.one_field === field.field);
|
|
247
|
+
if (!reverseRelation) {
|
|
248
|
+
return { type: 'o2m' };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
type: 'o2m',
|
|
252
|
+
collection: reverseRelation.collection,
|
|
253
|
+
many_field: reverseRelation.field,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildManyToManyRelation(field, snapshot) {
|
|
257
|
+
// Find the junction table relation that references this field
|
|
258
|
+
// This relation will have our field as meta.one_field
|
|
259
|
+
const junctionRelation = snapshot.relations.find((r) => r.meta?.one_field === field.field &&
|
|
260
|
+
r.meta?.one_collection === field.collection &&
|
|
261
|
+
r.collection !== field.collection);
|
|
262
|
+
if (!junctionRelation) {
|
|
263
|
+
return { type: 'm2m' };
|
|
264
|
+
}
|
|
265
|
+
// Find the other side of the junction (pointing to the target collection)
|
|
266
|
+
// This is stored in meta.junction_field
|
|
267
|
+
const targetRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection && r.field === junctionRelation.meta?.junction_field);
|
|
268
|
+
const targetCollection = targetRelation?.related_collection || 'directus_files';
|
|
269
|
+
const result = {
|
|
270
|
+
type: 'm2m',
|
|
271
|
+
collection: targetCollection,
|
|
272
|
+
junction: {
|
|
273
|
+
collection: junctionRelation.collection,
|
|
274
|
+
many_field: junctionRelation.field,
|
|
275
|
+
junction_field: junctionRelation.meta?.junction_field,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
if (junctionRelation.meta?.sort_field) {
|
|
279
|
+
result.junction.sort_field = junctionRelation.meta.sort_field;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function buildManyToAnyRelation(field, snapshot) {
|
|
284
|
+
// Find the junction table relation that references this field
|
|
285
|
+
// This relation will have our field as meta.one_field
|
|
286
|
+
const junctionRelation = snapshot.relations.find((r) => r.meta?.one_field === field.field && r.meta?.one_collection === field.collection);
|
|
287
|
+
if (!junctionRelation) {
|
|
288
|
+
return { type: 'm2a' };
|
|
289
|
+
}
|
|
290
|
+
// Find the polymorphic relation in the junction table
|
|
291
|
+
// This relation will have one_allowed_collections set
|
|
292
|
+
const polymorphicRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection &&
|
|
293
|
+
r.meta?.one_allowed_collections &&
|
|
294
|
+
r.meta.one_allowed_collections.length > 0);
|
|
295
|
+
if (!polymorphicRelation) {
|
|
296
|
+
return { type: 'm2a' };
|
|
297
|
+
}
|
|
298
|
+
// Find the relation back to our parent collection
|
|
299
|
+
const parentRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection &&
|
|
300
|
+
r.related_collection === field.collection &&
|
|
301
|
+
r.field !== polymorphicRelation.field);
|
|
302
|
+
const result = {
|
|
303
|
+
type: 'm2a',
|
|
304
|
+
one_allowed_collections: polymorphicRelation.meta?.one_allowed_collections,
|
|
305
|
+
junction: {
|
|
306
|
+
collection: junctionRelation.collection,
|
|
307
|
+
many_field: parentRelation?.field || `${field.collection}_id`,
|
|
308
|
+
junction_field: polymorphicRelation.field,
|
|
309
|
+
one_collection_field: polymorphicRelation.meta?.one_collection_field || 'collection',
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
const sortField = parentRelation?.meta?.sort_field || polymorphicRelation.meta?.sort_field;
|
|
313
|
+
if (sortField) {
|
|
314
|
+
result.junction.sort_field = sortField;
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define.js';
|
|
3
|
+
import prompts from './prompts/index.js';
|
|
4
|
+
const SystemPromptInputSchema = z.object({});
|
|
5
|
+
const SystemPromptValidateSchema = z.object({
|
|
6
|
+
promptOverride: z.union([z.string(), z.null()]).optional(),
|
|
7
|
+
});
|
|
8
|
+
export const system = defineTool({
|
|
9
|
+
name: 'system-prompt',
|
|
10
|
+
description: prompts.systemPromptDescription,
|
|
11
|
+
annotations: {
|
|
12
|
+
title: 'Directus - System Prompt',
|
|
13
|
+
},
|
|
14
|
+
inputSchema: SystemPromptInputSchema,
|
|
15
|
+
validateSchema: SystemPromptValidateSchema,
|
|
16
|
+
async handler({ args }) {
|
|
17
|
+
return {
|
|
18
|
+
type: 'text',
|
|
19
|
+
data: args.promptOverride || prompts.systemPrompt,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const triggerFlow: import("../types.js").ToolConfig<{
|
|
2
|
+
id: string | number;
|
|
3
|
+
collection: string;
|
|
4
|
+
keys?: (string | number)[] | undefined;
|
|
5
|
+
query?: Record<string, any> | undefined;
|
|
6
|
+
headers?: Record<string, any> | undefined;
|
|
7
|
+
data?: Record<string, any> | undefined;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFlowManager } from '../../flows.js';
|
|
4
|
+
import { FlowsService } from '../../services/flows.js';
|
|
5
|
+
import { defineTool } from '../define.js';
|
|
6
|
+
import { TriggerFlowInputSchema, TriggerFlowValidateSchema } from '../schema.js';
|
|
7
|
+
import prompts from './prompts/index.js';
|
|
8
|
+
export const triggerFlow = defineTool({
|
|
9
|
+
name: 'trigger-flow',
|
|
10
|
+
description: prompts.triggerFlow,
|
|
11
|
+
annotations: {
|
|
12
|
+
title: 'Directus - Trigger Flow',
|
|
13
|
+
},
|
|
14
|
+
inputSchema: TriggerFlowInputSchema,
|
|
15
|
+
validateSchema: TriggerFlowValidateSchema,
|
|
16
|
+
async handler({ args, schema, accountability }) {
|
|
17
|
+
const flowsService = new FlowsService({ schema, accountability });
|
|
18
|
+
const flow = await flowsService.readOne(args.id, {
|
|
19
|
+
filter: { status: { _eq: 'active' }, trigger: { _eq: 'manual' } },
|
|
20
|
+
fields: ['options'],
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Collection and Required selection are validated by the server.
|
|
24
|
+
* Required fields is an additional validation we do.
|
|
25
|
+
*/
|
|
26
|
+
const requiredFields = (flow.options?.['fields'] ?? [])
|
|
27
|
+
.filter((field) => field.meta?.required)
|
|
28
|
+
.map((field) => field.field);
|
|
29
|
+
for (const fieldName of requiredFields) {
|
|
30
|
+
if (!args.data || !(fieldName in args.data)) {
|
|
31
|
+
throw new InvalidPayloadError({ reason: `Required field "${fieldName}" is missing` });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const flowManager = getFlowManager();
|
|
35
|
+
const { result } = await flowManager.runWebhookFlow(`POST-${args.id}`, {
|
|
36
|
+
path: `/trigger/${args.id}`,
|
|
37
|
+
query: args.query ?? {},
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: {
|
|
40
|
+
collection: args.collection,
|
|
41
|
+
keys: args.keys,
|
|
42
|
+
...(args.data ?? {}),
|
|
43
|
+
},
|
|
44
|
+
headers: args.headers ?? {},
|
|
45
|
+
}, { accountability, schema });
|
|
46
|
+
return { type: 'text', data: result };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { Response } from 'express';
|
|
4
|
+
export declare class DirectusTransport implements Transport {
|
|
5
|
+
res: Response;
|
|
6
|
+
onerror?: (error: Error) => void;
|
|
7
|
+
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
constructor(res: Response);
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class DirectusTransport {
|
|
2
|
+
res;
|
|
3
|
+
onerror;
|
|
4
|
+
onmessage;
|
|
5
|
+
onclose;
|
|
6
|
+
constructor(res) {
|
|
7
|
+
this.res = res;
|
|
8
|
+
}
|
|
9
|
+
async start() {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
async send(message) {
|
|
13
|
+
this.res.json(message);
|
|
14
|
+
}
|
|
15
|
+
async close() {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { ZodType } from 'zod';
|
|
4
|
+
export type ToolResultBase = {
|
|
5
|
+
type?: 'text' | 'image' | 'audio';
|
|
6
|
+
url?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
export type TextToolResult = ToolResultBase & {
|
|
9
|
+
type: 'text';
|
|
10
|
+
data: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type AssetToolResult = ToolResultBase & {
|
|
13
|
+
type: 'image' | 'audio';
|
|
14
|
+
data: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
};
|
|
17
|
+
export type ToolResult = TextToolResult | AssetToolResult;
|
|
18
|
+
export type ToolHandler<T> = {
|
|
19
|
+
(options: {
|
|
20
|
+
args: T;
|
|
21
|
+
sanitizedQuery: Query;
|
|
22
|
+
schema: SchemaOverview;
|
|
23
|
+
accountability: Accountability | undefined;
|
|
24
|
+
}): Promise<ToolResult | undefined>;
|
|
25
|
+
};
|
|
26
|
+
export type ToolEndpoint<T> = {
|
|
27
|
+
(options: {
|
|
28
|
+
input: T;
|
|
29
|
+
data: unknown;
|
|
30
|
+
}): string[] | undefined;
|
|
31
|
+
};
|
|
32
|
+
export interface ToolConfig<T> {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
endpoint?: ToolEndpoint<T>;
|
|
36
|
+
admin?: boolean;
|
|
37
|
+
inputSchema: ZodType<any>;
|
|
38
|
+
validateSchema?: ZodType<T>;
|
|
39
|
+
annotations?: ToolAnnotations;
|
|
40
|
+
handler: ToolHandler<T>;
|
|
41
|
+
}
|
|
42
|
+
export interface Prompt {
|
|
43
|
+
name: string;
|
|
44
|
+
system_prompt?: string | null;
|
|
45
|
+
description?: string;
|
|
46
|
+
messages: {
|
|
47
|
+
role: 'user' | 'assistant';
|
|
48
|
+
text: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
export interface MCPOptions {
|
|
52
|
+
promptsCollection?: string;
|
|
53
|
+
allowDeletes?: boolean;
|
|
54
|
+
systemPromptEnabled?: boolean;
|
|
55
|
+
systemPrompt?: string | null;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,6 +18,7 @@ export function createMetrics() {
|
|
|
18
18
|
const env = useEnv();
|
|
19
19
|
const logger = useLogger();
|
|
20
20
|
const services = env['METRICS_SERVICES'] ?? [];
|
|
21
|
+
const metricNamePrefix = env['METRICS_NAME_PREFIX'] ?? 'directus_';
|
|
21
22
|
const aggregates = new Map();
|
|
22
23
|
/**
|
|
23
24
|
* Listen for PM2 metric data sync messages and add them to the aggregate
|
|
@@ -92,10 +93,10 @@ export function createMetrics() {
|
|
|
92
93
|
return null;
|
|
93
94
|
}
|
|
94
95
|
const client = env['DB_CLIENT'];
|
|
95
|
-
let metric = register.getSingleMetric(
|
|
96
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_connection_errors`);
|
|
96
97
|
if (!metric) {
|
|
97
98
|
metric = new Counter({
|
|
98
|
-
name:
|
|
99
|
+
name: `${metricNamePrefix}db_${client}_connection_errors`,
|
|
99
100
|
help: `${client} Database connection error count`,
|
|
100
101
|
});
|
|
101
102
|
}
|
|
@@ -106,10 +107,10 @@ export function createMetrics() {
|
|
|
106
107
|
return null;
|
|
107
108
|
}
|
|
108
109
|
const client = env['DB_CLIENT'];
|
|
109
|
-
let metric = register.getSingleMetric(
|
|
110
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_response_time_ms`);
|
|
110
111
|
if (!metric) {
|
|
111
112
|
metric = new Histogram({
|
|
112
|
-
name:
|
|
113
|
+
name: `${metricNamePrefix}db_${client}_response_time_ms`,
|
|
113
114
|
help: `${client} Database connection response time`,
|
|
114
115
|
buckets: [1, 10, 20, 40, 60, 80, 100, 200, 500, 750, 1000],
|
|
115
116
|
});
|
|
@@ -123,10 +124,10 @@ export function createMetrics() {
|
|
|
123
124
|
if (env['CACHE_STORE'] === 'redis' && redisConfigAvailable() !== true) {
|
|
124
125
|
return null;
|
|
125
126
|
}
|
|
126
|
-
let metric = register.getSingleMetric(
|
|
127
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}cache_${env['CACHE_STORE']}_connection_errors`);
|
|
127
128
|
if (!metric) {
|
|
128
129
|
metric = new Counter({
|
|
129
|
-
name:
|
|
130
|
+
name: `${metricNamePrefix}cache_${env['CACHE_STORE']}_connection_errors`,
|
|
130
131
|
help: 'Cache connection error count',
|
|
131
132
|
});
|
|
132
133
|
}
|
|
@@ -136,10 +137,10 @@ export function createMetrics() {
|
|
|
136
137
|
if (services.includes('redis') === false || redisConfigAvailable() !== true) {
|
|
137
138
|
return null;
|
|
138
139
|
}
|
|
139
|
-
let metric = register.getSingleMetric(
|
|
140
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}redis_connection_errors`);
|
|
140
141
|
if (!metric) {
|
|
141
142
|
metric = new Counter({
|
|
142
|
-
name:
|
|
143
|
+
name: `${metricNamePrefix}redis_connection_errors`,
|
|
143
144
|
help: 'Redis connection error count',
|
|
144
145
|
});
|
|
145
146
|
}
|
|
@@ -149,10 +150,10 @@ export function createMetrics() {
|
|
|
149
150
|
if (services.includes('storage') === false) {
|
|
150
151
|
return null;
|
|
151
152
|
}
|
|
152
|
-
let metric = register.getSingleMetric(
|
|
153
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}storage_${location}_connection_errors`);
|
|
153
154
|
if (!metric) {
|
|
154
155
|
metric = new Counter({
|
|
155
|
-
name:
|
|
156
|
+
name: `${metricNamePrefix}storage_${location}_connection_errors`,
|
|
156
157
|
help: `${location} storage connection error count`,
|
|
157
158
|
});
|
|
158
159
|
}
|
|
@@ -182,8 +183,8 @@ export function createMetrics() {
|
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
185
|
try {
|
|
185
|
-
await cache.set(`
|
|
186
|
-
await cache.delete(`
|
|
186
|
+
await cache.set(`directus-metric-${checkId}`, '1', 5);
|
|
187
|
+
await cache.delete(`directus-metric-${checkId}`);
|
|
187
188
|
}
|
|
188
189
|
catch {
|
|
189
190
|
metric.inc();
|
|
@@ -196,8 +197,8 @@ export function createMetrics() {
|
|
|
196
197
|
}
|
|
197
198
|
const redis = useRedis();
|
|
198
199
|
try {
|
|
199
|
-
await redis.set(`
|
|
200
|
-
await redis.del(`
|
|
200
|
+
await redis.set(`directus-metric-${checkId}`, '1');
|
|
201
|
+
await redis.del(`directus-metric-${checkId}`);
|
|
201
202
|
}
|
|
202
203
|
catch {
|
|
203
204
|
metric.inc();
|
|
@@ -215,17 +216,7 @@ export function createMetrics() {
|
|
|
215
216
|
continue;
|
|
216
217
|
}
|
|
217
218
|
try {
|
|
218
|
-
await disk.write(
|
|
219
|
-
const fileStream = await disk.read(`metric-${checkId}`);
|
|
220
|
-
fileStream.on('data', async () => {
|
|
221
|
-
try {
|
|
222
|
-
fileStream.destroy();
|
|
223
|
-
await disk.delete(`metric-${checkId}`);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
logger.error(error);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
219
|
+
await disk.write('directus-metric-file', Readable.from([checkId]));
|
|
229
220
|
}
|
|
230
221
|
catch {
|
|
231
222
|
metric.inc();
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Check if requested collection exists, and save it to req.collection
|
|
3
3
|
*/
|
|
4
|
-
import { ForbiddenError } from '@directus/errors';
|
|
5
4
|
import { systemCollectionRows } from '@directus/system-data';
|
|
6
5
|
import asyncHandler from '../utils/async-handler.js';
|
|
6
|
+
import { createCollectionForbiddenError } from '../permissions/modules/process-ast/utils/validate-path/create-error.js';
|
|
7
7
|
const collectionExists = asyncHandler(async (req, _res, next) => {
|
|
8
8
|
if (!req.params['collection'])
|
|
9
9
|
return next();
|
|
10
10
|
if (req.params['collection'] in req.schema.collections === false) {
|
|
11
|
-
throw
|
|
11
|
+
throw createCollectionForbiddenError('', req.params['collection']);
|
|
12
12
|
}
|
|
13
13
|
req.collection = req.params['collection'];
|
|
14
14
|
const systemCollectionRow = systemCollectionRows.find((collection) => {
|
|
@@ -2,10 +2,12 @@ import { defineOperationApi } from '@directus/extensions';
|
|
|
2
2
|
import { MailService } from '../../services/mail/index.js';
|
|
3
3
|
import { md } from '../../utils/md.js';
|
|
4
4
|
import { useLogger } from '../../logger/index.js';
|
|
5
|
+
import { useFlowsEmailRateLimiter } from './rate-limiter.js';
|
|
5
6
|
const logger = useLogger();
|
|
6
7
|
export default defineOperationApi({
|
|
7
8
|
id: 'mail',
|
|
8
|
-
handler: async ({ body, template, data, to, type, subject, cc, bcc, replyTo }, { accountability, database, getSchema }) => {
|
|
9
|
+
handler: async ({ body, template, data, to, type, subject, cc, bcc, replyTo }, { accountability, database, getSchema, flow }) => {
|
|
10
|
+
await useFlowsEmailRateLimiter(flow.id);
|
|
9
11
|
const mailService = new MailService({ schema: await getSchema({ database }), accountability, knex: database });
|
|
10
12
|
const mailObject = { to, subject, cc, bcc, replyTo };
|
|
11
13
|
const safeBody = typeof body !== 'string' ? JSON.stringify(body) : body;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useFlowsEmailRateLimiter(flow_id: string): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
|
|
3
|
+
import { createRateLimiter } from '../../rate-limiter.js';
|
|
4
|
+
import { toBoolean } from '@directus/utils';
|
|
5
|
+
import { EmailLimitExceededError } from '@directus/errors';
|
|
6
|
+
let emailRateLimiter;
|
|
7
|
+
const env = useEnv();
|
|
8
|
+
if (toBoolean(env['RATE_LIMITER_EMAIL_FLOWS_ENABLED']) === true) {
|
|
9
|
+
emailRateLimiter = createRateLimiter('RATE_LIMITER_EMAIL_FLOWS');
|
|
10
|
+
}
|
|
11
|
+
export async function useFlowsEmailRateLimiter(flow_id) {
|
|
12
|
+
if (!emailRateLimiter)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
await emailRateLimiter.consume(flow_id, 1);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err instanceof RateLimiterRes) {
|
|
19
|
+
throw new EmailLimitExceededError({
|
|
20
|
+
points: 'RATE_LIMITER_EMAIL_FLOWS_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_POINTS']) : undefined,
|
|
21
|
+
duration: 'RATE_LIMITER_EMAIL_FLOWS_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_DURATION']) : undefined,
|
|
22
|
+
message: 'RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE' in env
|
|
23
|
+
? String(env['RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE'])
|
|
24
|
+
: undefined,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|