@directus/api 32.0.2 → 32.1.1
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/controllers/extensions.js +6 -0
- package/dist/extensions/lib/installation/manager.js +13 -3
- package/dist/mcp/tools/prompts/flows.md +57 -12
- package/dist/mcp/tools/prompts/operations.md +57 -479
- package/dist/middleware/respond.js +5 -0
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/schema/parse-query.js +2 -2
- package/dist/services/graphql/utils/filter-replace-m2a.d.ts +7 -3
- package/dist/services/graphql/utils/filter-replace-m2a.js +15 -9
- package/dist/services/import-export.js +6 -4
- package/dist/services/mail/index.d.ts +15 -2
- package/dist/services/mail/index.js +5 -4
- package/dist/services/notifications.js +2 -0
- package/dist/services/tus/data-store.d.ts +1 -1
- package/dist/services/tus/data-store.js +5 -5
- package/dist/telemetry/utils/get-settings.js +13 -9
- package/dist/utils/validate-query.js +1 -1
- package/package.json +30 -30
|
@@ -74,6 +74,9 @@ router.get('/registry', asyncHandler(async (req, res, next) => {
|
|
|
74
74
|
return next();
|
|
75
75
|
}), respond);
|
|
76
76
|
router.get(`/registry/account/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
|
|
77
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
78
|
+
throw new ForbiddenError();
|
|
79
|
+
}
|
|
77
80
|
if (typeof req.params['pk'] !== 'string') {
|
|
78
81
|
throw new ForbiddenError();
|
|
79
82
|
}
|
|
@@ -86,6 +89,9 @@ router.get(`/registry/account/:pk(${UUID_REGEX})`, asyncHandler(async (req, res,
|
|
|
86
89
|
return next();
|
|
87
90
|
}), respond);
|
|
88
91
|
router.get(`/registry/extension/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
|
|
92
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
93
|
+
throw new ForbiddenError();
|
|
94
|
+
}
|
|
89
95
|
if (typeof req.params['pk'] !== 'string') {
|
|
90
96
|
throw new ForbiddenError();
|
|
91
97
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { ServiceUnavailableError } from '@directus/errors';
|
|
2
|
+
import { ErrorCode, isDirectusError, ServiceUnavailableError } from '@directus/errors';
|
|
3
3
|
import { EXTENSION_PKG_KEY, ExtensionManifest } from '@directus/extensions';
|
|
4
4
|
import { download } from '@directus/extensions-registry';
|
|
5
5
|
import DriverLocal from '@directus/storage-driver-local';
|
|
@@ -25,7 +25,13 @@ export class InstallationManager {
|
|
|
25
25
|
if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
|
|
26
26
|
options.registry = env['MARKETPLACE_REGISTRY'];
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
let tarReadableStream;
|
|
29
|
+
try {
|
|
30
|
+
tarReadableStream = await download(versionId, env['MARKETPLACE_TRUST'] === 'sandbox', options);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new ServiceUnavailableError({ service: 'marketplace', reason: 'Could not download the extension' }, { cause: error });
|
|
34
|
+
}
|
|
29
35
|
if (!tarReadableStream) {
|
|
30
36
|
throw new Error(`No readable stream returned from download`);
|
|
31
37
|
}
|
|
@@ -65,7 +71,11 @@ export class InstallationManager {
|
|
|
65
71
|
}
|
|
66
72
|
catch (err) {
|
|
67
73
|
logger.warn(err);
|
|
68
|
-
|
|
74
|
+
// rethrow marketplace servic unavailable
|
|
75
|
+
if (isDirectusError(err, ErrorCode.ServiceUnavailable)) {
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
throw new ServiceUnavailableError({ service: 'extensions', reason: 'Failed to extract the extension or write it to storage' }, { cause: err });
|
|
69
79
|
}
|
|
70
80
|
finally {
|
|
71
81
|
await rm(tempDir, { recursive: true });
|
|
@@ -247,12 +247,55 @@ UI button that users click to start flows
|
|
|
247
247
|
- Data chain variable syntax
|
|
248
248
|
- Operation-specific configuration
|
|
249
249
|
|
|
250
|
-
**Workflow
|
|
250
|
+
**Critical Workflow - Follow This Order:**
|
|
251
251
|
|
|
252
|
-
1. Create flow first
|
|
253
|
-
2.
|
|
254
|
-
3.
|
|
255
|
-
4.
|
|
252
|
+
1. Create flow first (using `flows` tool)
|
|
253
|
+
2. Create all operations with null resolve/reject initially (using `operations` tool)
|
|
254
|
+
3. Link operations together using UUIDs from step 2
|
|
255
|
+
4. Update flow to set first operation as entry point
|
|
256
|
+
|
|
257
|
+
**Why This Order:** Operations must exist before they can be referenced. UUIDs only available after creation.
|
|
258
|
+
|
|
259
|
+
**Complete Example:**
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
// Step 1: Create flow
|
|
263
|
+
{"action": "create", "data": {
|
|
264
|
+
"name": "Email on Post Published",
|
|
265
|
+
"trigger": "event",
|
|
266
|
+
"options": {"type": "action", "scope": ["items.create"], "collections": ["posts"]}
|
|
267
|
+
}}
|
|
268
|
+
// Returns: {"id": "flow-uuid-123"}
|
|
269
|
+
|
|
270
|
+
// Step 2: Create operations with null connections
|
|
271
|
+
{"action": "create", "data": {
|
|
272
|
+
"flow": "flow-uuid-123", "key": "check_status", "type": "condition",
|
|
273
|
+
"position_x": 19, "position_y": 1,
|
|
274
|
+
"options": {"filter": {"$trigger": {"payload": {"status": {"_eq": "published"}}}}},
|
|
275
|
+
"resolve": null, "reject": null
|
|
276
|
+
}}
|
|
277
|
+
// Returns: {"id": "condition-uuid-456"}
|
|
278
|
+
|
|
279
|
+
{"action": "create", "data": {
|
|
280
|
+
"flow": "flow-uuid-123", "key": "send_email", "type": "mail",
|
|
281
|
+
"position_x": 37, "position_y": 1,
|
|
282
|
+
"options": {"to": ["admin@example.com"], "subject": "New post", "body": "{{$trigger.payload.title}}"},
|
|
283
|
+
"resolve": null, "reject": null
|
|
284
|
+
}}
|
|
285
|
+
// Returns: {"id": "email-uuid-789"}
|
|
286
|
+
|
|
287
|
+
// Step 3: Link operations via UUIDs
|
|
288
|
+
{"action": "update", "key": "condition-uuid-456", "data": {
|
|
289
|
+
"resolve": "email-uuid-789"
|
|
290
|
+
}}
|
|
291
|
+
|
|
292
|
+
// Step 4: Set flow entry point
|
|
293
|
+
{"action": "update", "key": "flow-uuid-123", "data": {
|
|
294
|
+
"operation": "condition-uuid-456"
|
|
295
|
+
}}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
</operations_integration>
|
|
256
299
|
|
|
257
300
|
<flow_chaining>
|
|
258
301
|
|
|
@@ -319,15 +362,17 @@ UI button that users click to start flows
|
|
|
319
362
|
|
|
320
363
|
## Data Chain Access
|
|
321
364
|
|
|
322
|
-
|
|
365
|
+
Operations can access data using `{{ variable }}` syntax:
|
|
323
366
|
|
|
324
|
-
|
|
367
|
+
- `{{ $trigger.payload }}` - Trigger data
|
|
368
|
+
- `{{ $accountability.user }}` - User context
|
|
369
|
+
- `{{ $env.VARIABLE_NAME }}` - Environment variables
|
|
370
|
+
- `{{ operation_key }}` - Result from specific operation (recommended)
|
|
371
|
+
- `{{ operation_key.field }}` - Specific field from operation result
|
|
372
|
+
- `{{ $last }}` - Previous operation result (⚠️ avoid - breaks when reordered)
|
|
325
373
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
- `$env` - Environment variables
|
|
329
|
-
- `<operationKey>` - Result of specific operation (recommended)
|
|
330
|
-
- `$last` - Result of previous operation (avoid - breaks when reordered) </data_chain_warning>
|
|
374
|
+
**Always use operation keys** for reliable flows. If you reorder operations, `$last` will reference a different
|
|
375
|
+
operation. </data_chain_warning>
|
|
331
376
|
|
|
332
377
|
<real_world_examples>
|
|
333
378
|
|
|
@@ -146,309 +146,72 @@ can read existing operations to see if they are using extensions operations. </a
|
|
|
146
146
|
|
|
147
147
|
<workflow_creation>
|
|
148
148
|
|
|
149
|
-
## Workflow Creation Process
|
|
149
|
+
## Workflow Creation Process
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
**Critical Order:** Create flow first → Create operations with null resolve/reject → Link operations via UUIDs → Update
|
|
152
|
+
flow entry point.
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
### Step-by-Step Process:
|
|
156
|
-
|
|
157
|
-
1. **Create the flow** using the `flows` tool
|
|
158
|
-
2. **Create all operations** with null resolve/reject initially
|
|
159
|
-
3. **Link operations together** using the UUIDs returned from step 2
|
|
160
|
-
4. **Update the flow** to set the first operation as the entry point
|
|
161
|
-
|
|
162
|
-
### Why This Order Matters:
|
|
163
|
-
|
|
164
|
-
- Operations must exist before they can be referenced in resolve/reject fields
|
|
165
|
-
- UUIDs are only available after operations are created
|
|
166
|
-
- The flow needs at least one operation created before setting its entry point </workflow_steps>
|
|
167
|
-
|
|
168
|
-
<workflow_example>
|
|
169
|
-
|
|
170
|
-
### Complete Workflow Example:
|
|
171
|
-
|
|
172
|
-
```json
|
|
173
|
-
// Step 1: Create the flow first (using flows tool)
|
|
174
|
-
{
|
|
175
|
-
"action": "create",
|
|
176
|
-
"data": {
|
|
177
|
-
"name": "Email on Post Published",
|
|
178
|
-
"trigger": "event",
|
|
179
|
-
"options": {
|
|
180
|
-
"type": "action",
|
|
181
|
-
"scope": ["items.create"],
|
|
182
|
-
"collections": ["posts"]
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Returns: {"id": "flow-uuid-123", ...}
|
|
187
|
-
|
|
188
|
-
// Step 2: Create operations with null connections initially
|
|
189
|
-
{"action": "create", "data": {
|
|
190
|
-
"flow": "flow-uuid-123",
|
|
191
|
-
"key": "check_status",
|
|
192
|
-
"type": "condition",
|
|
193
|
-
"position_x": 19, // First operation position
|
|
194
|
-
"position_y": 1,
|
|
195
|
-
"options": {
|
|
196
|
-
"filter": {
|
|
197
|
-
"$trigger": {
|
|
198
|
-
"payload": {
|
|
199
|
-
"status": {"_eq": "published"}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
"resolve": null, // Set to null initially
|
|
205
|
-
"reject": null // Set to null initially
|
|
206
|
-
}}
|
|
207
|
-
// Returns: {"id": "condition-uuid-456", ...}
|
|
208
|
-
|
|
209
|
-
{"action": "create", "data": {
|
|
210
|
-
"flow": "flow-uuid-123",
|
|
211
|
-
"key": "send_email",
|
|
212
|
-
"type": "mail",
|
|
213
|
-
"position_x": 37, // Second operation position
|
|
214
|
-
"position_y": 1,
|
|
215
|
-
"options": {
|
|
216
|
-
"to": ["admin@example.com"],
|
|
217
|
-
"subject": "New post published",
|
|
218
|
-
"body": "Post '{{$trigger.payload.title}}' was published"
|
|
219
|
-
},
|
|
220
|
-
"resolve": null,
|
|
221
|
-
"reject": null
|
|
222
|
-
}}
|
|
223
|
-
// Returns: {"id": "email-uuid-789", ...}
|
|
224
|
-
|
|
225
|
-
// Step 3: Connect operations using UUIDs (NOT keys)
|
|
226
|
-
{"action": "update", "key": "condition-uuid-456", "data": {
|
|
227
|
-
"resolve": "email-uuid-789", // Use UUID from step 2
|
|
228
|
-
"reject": null // No error handling operation
|
|
229
|
-
}}
|
|
230
|
-
|
|
231
|
-
// Step 4: Update flow to set first operation (using flows tool)
|
|
232
|
-
{"action": "update", "key": "flow-uuid-123", "data": {
|
|
233
|
-
"operation": "condition-uuid-456" // First operation UUID
|
|
234
|
-
}}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
</workflow_example> </workflow_creation>
|
|
154
|
+
**See `flows` tool for complete workflow example with detailed steps.** </workflow_creation>
|
|
238
155
|
|
|
239
156
|
<positioning_system>
|
|
240
157
|
|
|
241
|
-
## Grid
|
|
242
|
-
|
|
243
|
-
**Grid Rules:**
|
|
244
|
-
|
|
245
|
-
- Each operation: 14x14 grid units
|
|
246
|
-
- Standard spacing: 18 units (19, 37, 55, 73...)
|
|
247
|
-
- Vertical start: `position_y: 1`
|
|
248
|
-
- Never use (0,0) - operations will overlap
|
|
249
|
-
|
|
250
|
-
**Common Patterns:**
|
|
251
|
-
|
|
252
|
-
```json
|
|
253
|
-
// Linear flow
|
|
254
|
-
{"position_x": 19, "position_y": 1} // First
|
|
255
|
-
{"position_x": 37, "position_y": 1} // Second
|
|
256
|
-
{"position_x": 55, "position_y": 1} // Third
|
|
257
|
-
|
|
258
|
-
// Branching (success/error)
|
|
259
|
-
{"position_x": 19, "position_y": 1} // Main
|
|
260
|
-
{"position_x": 37, "position_y": 1} // Success (same row)
|
|
261
|
-
{"position_x": 37, "position_y": 19} // Error (lower row)
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
</positioning_system>
|
|
265
|
-
|
|
266
|
-
<operation_examples> <condition> Evaluates filter rules to determine path
|
|
267
|
-
|
|
268
|
-
```json
|
|
269
|
-
{
|
|
270
|
-
"type": "condition",
|
|
271
|
-
"options": {
|
|
272
|
-
"filter": {
|
|
273
|
-
"$trigger": {
|
|
274
|
-
"payload": {
|
|
275
|
-
"status": { "_eq": "published" }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
<filter_examples>
|
|
284
|
-
|
|
285
|
-
```json
|
|
286
|
-
// Check if field exists
|
|
287
|
-
{
|
|
288
|
-
"filter": {
|
|
289
|
-
"$trigger": {
|
|
290
|
-
"payload": {
|
|
291
|
-
"website": {"_nnull": true}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Multiple conditions (AND) - CORRECTED SYNTAX
|
|
298
|
-
{
|
|
299
|
-
"filter": {
|
|
300
|
-
"$trigger": {
|
|
301
|
-
"payload": {
|
|
302
|
-
"_and": [
|
|
303
|
-
{"status": {"_eq": "published"}},
|
|
304
|
-
{"featured": {"_eq": true}}
|
|
305
|
-
]
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
</filter_examples> </condition>
|
|
313
|
-
|
|
314
|
-
<item_operations> **Create Items:**
|
|
158
|
+
## Grid Positioning
|
|
315
159
|
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
"type": "item-create",
|
|
319
|
-
"options": {
|
|
320
|
-
"collection": "notifications",
|
|
321
|
-
"permissions": "$trigger",
|
|
322
|
-
"emitEvents": true,
|
|
323
|
-
"payload": {
|
|
324
|
-
"title": "{{ $trigger.payload.title }}",
|
|
325
|
-
"user": "{{ $accountability.user }}"
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
160
|
+
**Rules:** 14x14 units, spacing every 18 units (example 19/37/55/73). Never (0,0). Start `position_y: 1`.
|
|
330
161
|
|
|
331
|
-
**
|
|
162
|
+
**Patterns:** Linear (19,1)→(37,1)→(55,1). Branching: success (37,1), error (37,19). </positioning_system>
|
|
332
163
|
|
|
333
|
-
|
|
334
|
-
{
|
|
335
|
-
"type": "item-read",
|
|
336
|
-
"options": {
|
|
337
|
-
"collection": "products",
|
|
338
|
-
"permissions": "$full",
|
|
339
|
-
"query": {
|
|
340
|
-
"filter": { "status": { "_eq": "active" } },
|
|
341
|
-
"limit": 10
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
**Update Items:**
|
|
164
|
+
<operation_examples> **condition** - Evaluates filter rules
|
|
348
165
|
|
|
349
166
|
```json
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
"options": {
|
|
353
|
-
"collection": "orders",
|
|
354
|
-
"permissions": "$trigger",
|
|
355
|
-
"emitEvents": true,
|
|
356
|
-
"key": "{{ $trigger.payload.id }}",
|
|
357
|
-
"payload": { "status": "processed" }
|
|
358
|
-
}
|
|
359
|
-
}
|
|
167
|
+
{ "type": "condition", "options": { "filter": { "$trigger": { "payload": { "status": { "_eq": "published" } } } } } }
|
|
168
|
+
// Multiple: {"_and": [{"status": {"_eq": "published"}}, {"featured": {"_eq": true}}]}
|
|
360
169
|
```
|
|
361
170
|
|
|
362
|
-
**
|
|
171
|
+
**item-create/read/update/delete** - CRUD operations
|
|
363
172
|
|
|
364
173
|
```json
|
|
365
|
-
{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
"permissions": "$full",
|
|
370
|
-
"key": ["{{ read_items[0].id }}"]
|
|
371
|
-
}
|
|
372
|
-
}
|
|
174
|
+
{"type": "item-create", "options": {"collection": "notifications", "permissions": "$trigger", "payload": {"title": "{{ $trigger.payload.title }}"}}}
|
|
175
|
+
{"type": "item-read", "options": {"collection": "products", "query": {"filter": {"status": {"_eq": "active"}}}}}
|
|
176
|
+
{"type": "item-update", "options": {"collection": "orders", "key": "{{ $trigger.payload.id }}", "payload": {"status": "processed"}}}
|
|
177
|
+
{"type": "item-delete", "options": {"collection": "temp_data", "key": ["{{ read_items[0].id }}"]}}
|
|
373
178
|
```
|
|
374
179
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<exec>
|
|
378
|
-
Execute custom JavaScript/TypeScript in isolated sandbox
|
|
379
|
-
|
|
380
|
-
**⚠️ SECURITY WARNING**: Scripts run sandboxed with NO file system or network access
|
|
180
|
+
**exec** - Custom JavaScript/TypeScript (sandboxed, no file/network access)
|
|
381
181
|
|
|
382
182
|
```json
|
|
383
183
|
{
|
|
384
184
|
"type": "exec",
|
|
385
185
|
"options": {
|
|
386
|
-
"code": "module.exports = async function(data) {\n
|
|
186
|
+
"code": "module.exports = async function(data) {\n const result = data.$trigger.payload.value * 2;\n return { result, processed: true };\n}"
|
|
387
187
|
}
|
|
388
188
|
}
|
|
389
189
|
```
|
|
390
190
|
|
|
391
|
-
**
|
|
392
|
-
|
|
393
|
-
<mail>
|
|
394
|
-
Send email notifications with optional templates
|
|
191
|
+
**mail** - Send email (markdown/wysiwyg/template)
|
|
395
192
|
|
|
396
193
|
```json
|
|
397
194
|
{
|
|
398
195
|
"type": "mail",
|
|
399
196
|
"options": {
|
|
400
|
-
"to": ["user@example.com"
|
|
197
|
+
"to": ["user@example.com"],
|
|
401
198
|
"subject": "Order Confirmation",
|
|
402
|
-
"
|
|
403
|
-
"body": "Your order {{ $trigger.payload.order_id }} has been confirmed.",
|
|
404
|
-
"cc": ["cc@example.com"], // Optional
|
|
405
|
-
"bcc": ["bcc@example.com"], // Optional
|
|
406
|
-
"replyTo": ["reply@example.com"] // Optional
|
|
199
|
+
"body": "Order {{ $trigger.payload.order_id }}"
|
|
407
200
|
}
|
|
408
201
|
}
|
|
202
|
+
// Template: {"type": "template", "template": "welcome-email", "data": {"username": "{{ $trigger.payload.name }}"}}
|
|
409
203
|
```
|
|
410
204
|
|
|
411
|
-
**
|
|
412
|
-
|
|
413
|
-
```json
|
|
414
|
-
{
|
|
415
|
-
"type": "mail",
|
|
416
|
-
"options": {
|
|
417
|
-
"to": ["{{ $trigger.payload.email }}"],
|
|
418
|
-
"subject": "Welcome!",
|
|
419
|
-
"type": "template",
|
|
420
|
-
"template": "welcome-email", // Template name (default: "base")
|
|
421
|
-
"data": {
|
|
422
|
-
"username": "{{ $trigger.payload.name }}",
|
|
423
|
-
"activation_url": "https://example.com/activate/{{ $trigger.payload.token }}"
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
</mail>
|
|
430
|
-
|
|
431
|
-
<notification>
|
|
432
|
-
Send in-app notifications to users
|
|
205
|
+
**notification** - In-app notifications
|
|
433
206
|
|
|
434
207
|
```json
|
|
435
208
|
{
|
|
436
209
|
"type": "notification",
|
|
437
|
-
"options": {
|
|
438
|
-
"recipient": ["{{ $accountability.user }}"], // User ID(s) to notify
|
|
439
|
-
"subject": "Task Complete",
|
|
440
|
-
"message": "Your export is ready for download",
|
|
441
|
-
"permissions": "$trigger",
|
|
442
|
-
"collection": "exports", // Optional: Related collection
|
|
443
|
-
"item": "{{ create_export.id }}" // Optional: Related item ID
|
|
444
|
-
}
|
|
210
|
+
"options": { "recipient": ["{{ $accountability.user }}"], "subject": "Task Complete", "message": "Export ready" }
|
|
445
211
|
}
|
|
446
212
|
```
|
|
447
213
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
<request>
|
|
451
|
-
Make HTTP requests
|
|
214
|
+
**request** - HTTP requests (headers must be array of objects, body as stringified JSON)
|
|
452
215
|
|
|
453
216
|
```json
|
|
454
217
|
{
|
|
@@ -456,146 +219,57 @@ Make HTTP requests
|
|
|
456
219
|
"options": {
|
|
457
220
|
"method": "POST",
|
|
458
221
|
"url": "https://api.example.com/webhook",
|
|
459
|
-
"headers": [
|
|
460
|
-
|
|
461
|
-
"header": "Authorization",
|
|
462
|
-
"value": "Bearer {{ $env.API_TOKEN }}"
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
"header": "Content-Type",
|
|
466
|
-
"value": "application/json"
|
|
467
|
-
}
|
|
468
|
-
],
|
|
469
|
-
"body": "{\"data\": \"{{ process_data }}\", \"timestamp\": \"{{ $trigger.timestamp }}\"}"
|
|
222
|
+
"headers": [{ "header": "Authorization", "value": "Bearer {{ $env.API_TOKEN }}" }],
|
|
223
|
+
"body": "{\"data\": \"{{ process_data }}\"}"
|
|
470
224
|
}
|
|
471
225
|
}
|
|
472
226
|
```
|
|
473
227
|
|
|
474
|
-
**
|
|
475
|
-
|
|
476
|
-
```json
|
|
477
|
-
{
|
|
478
|
-
"type": "request",
|
|
479
|
-
"options": {
|
|
480
|
-
"method": "POST",
|
|
481
|
-
"url": "https://api.netlify.com/build_hooks/your-hook-id",
|
|
482
|
-
"headers": [
|
|
483
|
-
{
|
|
484
|
-
"header": "User-Agent",
|
|
485
|
-
"value": "Directus-Flow/1.0"
|
|
486
|
-
}
|
|
487
|
-
],
|
|
488
|
-
"body": "{\"trigger\": \"content_updated\", \"item_id\": \"{{ $trigger.payload.id }}\"}"
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
</request>
|
|
494
|
-
|
|
495
|
-
<json_web_token> Sign, verify, or decode JWT tokens - **CONSOLIDATED EXAMPLE**
|
|
228
|
+
**json-web-token** - Sign/verify/decode JWT
|
|
496
229
|
|
|
497
230
|
```json
|
|
498
231
|
{
|
|
499
232
|
"type": "json-web-token",
|
|
500
233
|
"options": {
|
|
501
|
-
"operation": "sign",
|
|
502
|
-
|
|
503
|
-
// For SIGN operations:
|
|
504
|
-
"payload": {
|
|
505
|
-
"userId": "{{ $trigger.payload.user }}",
|
|
506
|
-
"role": "{{ $trigger.payload.role }}"
|
|
507
|
-
},
|
|
234
|
+
"operation": "sign",
|
|
235
|
+
"payload": { "userId": "{{ $trigger.payload.user }}" },
|
|
508
236
|
"secret": "{{ $env.JWT_SECRET }}",
|
|
509
|
-
"options": {
|
|
510
|
-
"expiresIn": "1h",
|
|
511
|
-
"algorithm": "HS256"
|
|
512
|
-
},
|
|
513
|
-
|
|
514
|
-
// For VERIFY/DECODE operations:
|
|
515
|
-
"token": "{{ $trigger.payload.token }}"
|
|
516
|
-
// "secret": "{{ $env.JWT_SECRET }}", // Required for verify, not for decode
|
|
517
|
-
// "options": {"algorithms": ["HS256"]}, // For verify
|
|
518
|
-
// "options": {"complete": true} // For decode
|
|
237
|
+
"options": { "expiresIn": "1h" }
|
|
519
238
|
}
|
|
520
239
|
}
|
|
240
|
+
// Verify: {"operation": "verify", "token": "{{ $trigger.payload.token }}", "secret": "{{ $env.JWT_SECRET }}"}
|
|
521
241
|
```
|
|
522
242
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
<other_operations> **Transform JSON:**
|
|
243
|
+
**transform** - Create custom JSON payloads
|
|
526
244
|
|
|
527
245
|
```json
|
|
528
246
|
{
|
|
529
247
|
"type": "transform",
|
|
530
|
-
"options": {
|
|
531
|
-
"json": {
|
|
532
|
-
"combined": {
|
|
533
|
-
"user": "{{ $accountability.user }}",
|
|
534
|
-
"items": "{{ read_items }}",
|
|
535
|
-
"timestamp": "{{ $trigger.timestamp }}"
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
248
|
+
"options": { "json": { "combined": { "user": "{{ $accountability.user }}", "items": "{{ read_items }}" } } }
|
|
539
249
|
}
|
|
540
250
|
```
|
|
541
251
|
|
|
542
|
-
**
|
|
252
|
+
**trigger** - Execute another flow
|
|
543
253
|
|
|
544
254
|
```json
|
|
545
255
|
{
|
|
546
256
|
"type": "trigger",
|
|
547
|
-
"options": {
|
|
548
|
-
"flow": "other-flow-uuid",
|
|
549
|
-
"payload": { "data": "{{ transform_result }}" },
|
|
550
|
-
"iterationMode": "parallel", // "parallel", "serial", "batch"
|
|
551
|
-
"batchSize": 10 // Only for batch mode
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
**Sleep:**
|
|
557
|
-
|
|
558
|
-
```json
|
|
559
|
-
{
|
|
560
|
-
"type": "sleep",
|
|
561
|
-
"options": { "milliseconds": 5000 }
|
|
257
|
+
"options": { "flow": "flow-uuid", "payload": { "data": "{{ transform_result }}" }, "iterationMode": "parallel" }
|
|
562
258
|
}
|
|
563
259
|
```
|
|
564
260
|
|
|
565
|
-
**
|
|
261
|
+
**sleep/log/throw-error** - Utilities
|
|
566
262
|
|
|
567
263
|
```json
|
|
568
|
-
{
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
264
|
+
{"type": "sleep", "options": {"milliseconds": 5000}}
|
|
265
|
+
{"type": "log", "options": {"message": "Processing {{ $trigger.payload.id }}"}}
|
|
266
|
+
{"type": "throw-error", "options": {"code": "CUSTOM_ERROR", "status": "400", "message": "Invalid data"}}
|
|
572
267
|
```
|
|
573
268
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
```json
|
|
577
|
-
{
|
|
578
|
-
"type": "throw-error",
|
|
579
|
-
"options": {
|
|
580
|
-
"code": "CUSTOM_ERROR",
|
|
581
|
-
"status": "400",
|
|
582
|
-
"message": "Invalid data: {{ $trigger.payload.error_details }}"
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
</other_operations> </operation_examples>
|
|
588
|
-
|
|
589
|
-
<data_chain_variables> Use `{{ variable }}` syntax to access data:
|
|
590
|
-
|
|
591
|
-
- `{{ $trigger.payload }}` - Trigger data
|
|
592
|
-
- `{{ $accountability.user }}` - User context
|
|
593
|
-
- `{{ operation_key }}` - Result from specific operation (recommended)
|
|
594
|
-
- `{{ operation_key.field }}` - Specific field from operation result
|
|
269
|
+
</operation_examples>
|
|
595
270
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
use specific operation keys like `{{ operation_key }}` for reliable, maintainable flows. </data_chain_variables>
|
|
271
|
+
<data_chain_variables> **Data Chain:** Use `{{ operation_key }}` to access results, `{{ $trigger.payload }}` for trigger
|
|
272
|
+
data. Avoid `{{ $last }}` (breaks when reordered). See `flows` tool for complete syntax. </data_chain_variables>
|
|
599
273
|
|
|
600
274
|
<permission_options> For operations that support permissions:
|
|
601
275
|
|
|
@@ -604,118 +278,22 @@ use specific operation keys like `{{ operation_key }}` for reliable, maintainabl
|
|
|
604
278
|
- `$full` - Use full system permissions
|
|
605
279
|
- `role-uuid` - Use specific role's permissions </permission_options>
|
|
606
280
|
|
|
607
|
-
<real_world_patterns> <data_processing_pipeline>
|
|
608
|
-
|
|
609
|
-
### Data Processing Pipeline
|
|
610
|
-
|
|
611
|
-
Read → Transform → Update pattern:
|
|
612
|
-
|
|
613
|
-
```json
|
|
614
|
-
// 1. Read with relations
|
|
615
|
-
{
|
|
616
|
-
"flow": "flow-uuid", "key": "invoice", "type": "item-read",
|
|
617
|
-
"position_x": 19, "position_y": 1,
|
|
618
|
-
"options": {
|
|
619
|
-
"collection": "os_invoices",
|
|
620
|
-
"key": ["{{$trigger.payload.invoice}}"],
|
|
621
|
-
"query": {"fields": ["*", "line_items.*", "payments.*"]}
|
|
622
|
-
},
|
|
623
|
-
"resolve": "calc-operation-uuid"
|
|
624
|
-
}
|
|
625
|
-
// 2. Calculate totals
|
|
626
|
-
{
|
|
627
|
-
"flow": "flow-uuid", "key": "calculations", "type": "exec",
|
|
628
|
-
"position_x": 37, "position_y": 1,
|
|
629
|
-
"options": {
|
|
630
|
-
"code": "module.exports = async function(data) {\n const invoice = data.invoice;\n const subtotal = invoice.line_items.reduce((sum, item) => sum + (item.price * item.quantity), 0);\n const tax = subtotal * 0.08;\n return { subtotal, tax, total: subtotal + tax };\n}"
|
|
631
|
-
},
|
|
632
|
-
"resolve": "update-operation-uuid"
|
|
633
|
-
}
|
|
634
|
-
// 3. Update with results
|
|
635
|
-
{
|
|
636
|
-
"flow": "flow-uuid", "key": "update_invoice", "type": "item-update",
|
|
637
|
-
"position_x": 55, "position_y": 1,
|
|
638
|
-
"options": {
|
|
639
|
-
"collection": "os_invoices",
|
|
640
|
-
"payload": "{{calculations}}",
|
|
641
|
-
"key": ["{{$trigger.payload.invoice}}"]
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
</data_processing_pipeline>
|
|
647
|
-
|
|
648
|
-
<error_handling_branching>
|
|
649
|
-
|
|
650
|
-
### Error Handling with Branching
|
|
651
|
-
|
|
652
|
-
```json
|
|
653
|
-
// Main operation with error handling
|
|
654
|
-
{
|
|
655
|
-
"flow": "flow-uuid", "key": "main_operation", "type": "request",
|
|
656
|
-
"position_x": 19, "position_y": 1,
|
|
657
|
-
"resolve": "success-operation-uuid",
|
|
658
|
-
"reject": "error-operation-uuid"
|
|
659
|
-
}
|
|
660
|
-
// Success path
|
|
661
|
-
{
|
|
662
|
-
"flow": "flow-uuid", "key": "success_notification", "type": "notification",
|
|
663
|
-
"position_x": 37, "position_y": 1
|
|
664
|
-
}
|
|
665
|
-
// Error path (lower row)
|
|
666
|
-
{
|
|
667
|
-
"flow": "flow-uuid", "key": "error_log", "type": "log",
|
|
668
|
-
"position_x": 37, "position_y": 19
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
</error_handling_branching> </real_world_patterns>
|
|
673
|
-
|
|
674
281
|
<common_mistakes>
|
|
675
282
|
|
|
676
|
-
1.
|
|
677
|
-
2.
|
|
678
|
-
3.
|
|
679
|
-
4.
|
|
680
|
-
5.
|
|
681
|
-
6.
|
|
682
|
-
7.
|
|
683
|
-
8.
|
|
684
|
-
9.
|
|
685
|
-
10.
|
|
283
|
+
1. Create flow first, never operations without flow
|
|
284
|
+
2. Use UUIDs in resolve/reject, NOT keys
|
|
285
|
+
3. Create operations before referencing them
|
|
286
|
+
4. No duplicate keys within same flow
|
|
287
|
+
5. Avoid circular resolve/reject references
|
|
288
|
+
6. Set positions (not 0,0)
|
|
289
|
+
7. Use nested objects in filters, NOT dot notation
|
|
290
|
+
8. Request headers as array of objects, body as stringified JSON
|
|
291
|
+
9. Pass native objects in data (except request body)
|
|
292
|
+
10. ALWAYS pass native objects in data (EXCEPTIONS: - request body for `request` operation - code in `exec` operations)
|
|
293
|
+
11. No `$NOW` variable - use exec operation: `return { now: new Date().toISOString() };` </common_mistakes>
|
|
686
294
|
|
|
687
295
|
<troubleshooting>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
- Verify the operation UUID exists by reading operations for the flow
|
|
694
|
-
- Check that you're using UUIDs (36 characters) not keys (short names)
|
|
695
|
-
- Ensure operations are created before being referenced </invalid_foreign_key>
|
|
696
|
-
|
|
697
|
-
<operation_not_executing>
|
|
698
|
-
|
|
699
|
-
### Operation Not Executing
|
|
700
|
-
|
|
701
|
-
- Check the resolve/reject chain for breaks
|
|
702
|
-
- Verify the first operation is set as the flow's `operation` field
|
|
703
|
-
- Confirm all required operation options are provided </operation_not_executing>
|
|
704
|
-
|
|
705
|
-
<overlapping_operations>
|
|
706
|
-
|
|
707
|
-
### Overlapping Operations in Visual Editor
|
|
708
|
-
|
|
709
|
-
If operations appear stacked at (0,0) in the flow editor:
|
|
710
|
-
|
|
711
|
-
```json
|
|
712
|
-
// Fix by updating each operation's position
|
|
713
|
-
{"action": "update", "key": "operation-uuid", "data": {
|
|
714
|
-
"position_x": 19, "position_y": 1
|
|
715
|
-
}}
|
|
716
|
-
{"action": "update", "key": "other-operation-uuid", "data": {
|
|
717
|
-
"position_x": 37, "position_y": 1
|
|
718
|
-
}}
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
</overlapping_operations> </troubleshooting>
|
|
296
|
+
**Invalid foreign key:** Operation UUID doesn't exist. Use UUIDs (36 chars), not keys. Create operations before referencing.
|
|
297
|
+
**Not executing:** Check resolve/reject chain, verify flow.operation set, confirm required options provided.
|
|
298
|
+
**Overlapping (0,0):** Update positions: `{"action": "update", "key": "uuid", "data": {"position_x": 19, "position_y": 1}}`
|
|
299
|
+
</troubleshooting>
|
|
@@ -74,6 +74,11 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
74
74
|
res.set('Content-Type', 'text/csv');
|
|
75
75
|
return res.status(200).send(exportService.transform(res.locals['payload']?.data, 'csv'));
|
|
76
76
|
}
|
|
77
|
+
if (req.sanitizedQuery.export === 'csv_utf8') {
|
|
78
|
+
res.attachment(`${filename}.csv`);
|
|
79
|
+
res.set('Content-Type', 'text/csv; charset=utf-8');
|
|
80
|
+
return res.status(200).send(exportService.transform(res.locals['payload']?.data, 'csv_utf8'));
|
|
81
|
+
}
|
|
77
82
|
if (req.sanitizedQuery.export === 'yaml') {
|
|
78
83
|
res.attachment(`${filename}.yaml`);
|
|
79
84
|
res.set('Content-Type', 'text/yaml');
|
|
@@ -23,10 +23,10 @@ export async function resolveQuery(gql, info) {
|
|
|
23
23
|
query = await getAggregateQuery(args, selections, gql.schema, gql.accountability, collection);
|
|
24
24
|
}
|
|
25
25
|
else {
|
|
26
|
-
query = await getQuery(args, gql.schema, selections, info.variableValues, gql.accountability, collection);
|
|
27
26
|
if (collection.endsWith('_by_id') && collection in gql.schema.collections === false) {
|
|
28
27
|
collection = collection.slice(0, -6);
|
|
29
28
|
}
|
|
29
|
+
query = await getQuery(args, gql.schema, selections, info.variableValues, gql.accountability, collection);
|
|
30
30
|
if (collection.endsWith('_by_version') && collection in gql.schema.collections === false) {
|
|
31
31
|
collection = collection.slice(0, -11);
|
|
32
32
|
query.versionRaw = true;
|
|
@@ -96,9 +96,9 @@ export async function getQuery(rawQuery, schema, selections, variableValues, acc
|
|
|
96
96
|
query.deep = replaceFuncs(query.deep);
|
|
97
97
|
if (collection) {
|
|
98
98
|
if (query.filter) {
|
|
99
|
-
query.filter = filterReplaceM2A(query.filter, collection, schema);
|
|
99
|
+
query.filter = filterReplaceM2A(query.filter, collection, schema, { aliasMap: query.alias });
|
|
100
100
|
}
|
|
101
|
-
query.deep = filterReplaceM2ADeep(query.deep, collection, schema);
|
|
101
|
+
query.deep = filterReplaceM2ADeep(query.deep, collection, schema, { aliasMap: query.alias });
|
|
102
102
|
}
|
|
103
103
|
validateQuery(query);
|
|
104
104
|
return query;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type { Filter, NestedDeepQuery, SchemaOverview } from '@directus/types';
|
|
2
|
-
export declare function filterReplaceM2A(filter_arg: Filter, collection: string, schema: SchemaOverview
|
|
3
|
-
|
|
1
|
+
import type { Filter, NestedDeepQuery, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
export declare function filterReplaceM2A(filter_arg: Filter, collection: string, schema: SchemaOverview, options?: {
|
|
3
|
+
aliasMap?: Query['alias'];
|
|
4
|
+
}): any;
|
|
5
|
+
export declare function filterReplaceM2ADeep(deep_arg: NestedDeepQuery | null | undefined, collection: string, schema: SchemaOverview, options?: {
|
|
6
|
+
aliasMap?: Query['alias'];
|
|
7
|
+
}): any;
|
|
@@ -1,42 +1,48 @@
|
|
|
1
1
|
import { getRelation } from '@directus/utils';
|
|
2
2
|
import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
3
|
-
export function filterReplaceM2A(filter_arg, collection, schema) {
|
|
3
|
+
export function filterReplaceM2A(filter_arg, collection, schema, options) {
|
|
4
4
|
const filter = filter_arg;
|
|
5
5
|
for (const key in filter) {
|
|
6
|
-
const
|
|
6
|
+
const parts = key.split('__');
|
|
7
|
+
let field = parts[0];
|
|
8
|
+
const any_collection = parts[1];
|
|
7
9
|
if (!field)
|
|
8
10
|
continue;
|
|
11
|
+
field = options?.aliasMap?.[field] ?? field;
|
|
9
12
|
const relation = getRelation(schema.relations, collection, field);
|
|
10
13
|
const type = relation ? getRelationType({ relation, collection, field }) : null;
|
|
11
14
|
if (type === 'o2m' && relation) {
|
|
12
|
-
filter[key] = filterReplaceM2A(filter[key], relation.collection, schema);
|
|
15
|
+
filter[key] = filterReplaceM2A(filter[key], relation.collection, schema, options);
|
|
13
16
|
}
|
|
14
17
|
else if (type === 'm2o' && relation) {
|
|
15
|
-
filter[key] = filterReplaceM2A(filter[key], relation.related_collection, schema);
|
|
18
|
+
filter[key] = filterReplaceM2A(filter[key], relation.related_collection, schema, options);
|
|
16
19
|
}
|
|
17
20
|
else if (type === 'a2o' &&
|
|
18
21
|
relation &&
|
|
19
22
|
any_collection &&
|
|
20
23
|
relation.meta?.one_allowed_collections?.includes(any_collection)) {
|
|
21
|
-
filter[`${field}:${any_collection}`] = filterReplaceM2A(filter[key], any_collection, schema);
|
|
24
|
+
filter[`${field}:${any_collection}`] = filterReplaceM2A(filter[key], any_collection, schema, options);
|
|
22
25
|
delete filter[key];
|
|
23
26
|
}
|
|
24
27
|
else if (Array.isArray(filter[key])) {
|
|
25
|
-
filter[key] = filter[key].map((item) => filterReplaceM2A(item, collection, schema));
|
|
28
|
+
filter[key] = filter[key].map((item) => filterReplaceM2A(item, collection, schema, options));
|
|
26
29
|
}
|
|
27
30
|
else if (typeof filter[key] === 'object') {
|
|
28
|
-
filter[key] = filterReplaceM2A(filter[key], collection, schema);
|
|
31
|
+
filter[key] = filterReplaceM2A(filter[key], collection, schema, options);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
return filter;
|
|
32
35
|
}
|
|
33
|
-
export function filterReplaceM2ADeep(deep_arg, collection, schema) {
|
|
36
|
+
export function filterReplaceM2ADeep(deep_arg, collection, schema, options) {
|
|
34
37
|
const deep = deep_arg;
|
|
35
38
|
for (const key in deep) {
|
|
36
39
|
if (key.startsWith('_') === false) {
|
|
37
|
-
const
|
|
40
|
+
const parts = key.split('__');
|
|
41
|
+
let field = parts[0];
|
|
42
|
+
const any_collection = parts[1];
|
|
38
43
|
if (!field)
|
|
39
44
|
continue;
|
|
45
|
+
field = options?.aliasMap?.[field] || deep._alias?.[field] || field;
|
|
40
46
|
const relation = getRelation(schema.relations, collection, field);
|
|
41
47
|
if (!relation)
|
|
42
48
|
continue;
|
|
@@ -443,6 +443,7 @@ export class ExportService {
|
|
|
443
443
|
throw new Error('Failed to create temporary file for export');
|
|
444
444
|
const mimeTypes = {
|
|
445
445
|
csv: 'text/csv',
|
|
446
|
+
csv_utf8: 'text/csv; charset=utf-8',
|
|
446
447
|
json: 'application/json',
|
|
447
448
|
xml: 'text/xml',
|
|
448
449
|
yaml: 'text/yaml',
|
|
@@ -485,7 +486,7 @@ export class ExportService {
|
|
|
485
486
|
readCount += result.length;
|
|
486
487
|
if (result.length) {
|
|
487
488
|
let csvHeadings = null;
|
|
488
|
-
if (format
|
|
489
|
+
if (format.startsWith('csv')) {
|
|
489
490
|
if (!query.fields)
|
|
490
491
|
query.fields = ['*'];
|
|
491
492
|
// to ensure the all headings are included in the CSV file, all possible fields need to be determined.
|
|
@@ -593,14 +594,15 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
|
|
|
593
594
|
}
|
|
594
595
|
return string;
|
|
595
596
|
}
|
|
596
|
-
if (format
|
|
597
|
+
if (format.startsWith('csv')) {
|
|
597
598
|
if (input.length === 0)
|
|
598
599
|
return '';
|
|
599
600
|
const transforms = [CSVTransforms.flatten({ separator: '.' })];
|
|
600
601
|
const header = options?.includeHeader !== false;
|
|
602
|
+
const withBOM = format === 'csv_utf8';
|
|
601
603
|
const transformOptions = options?.fields
|
|
602
|
-
? { transforms, header, fields: options?.fields }
|
|
603
|
-
: { transforms, header };
|
|
604
|
+
? { transforms, header, fields: options?.fields, withBOM }
|
|
605
|
+
: { transforms, header, withBOM };
|
|
604
606
|
let string = new CSVParser(transformOptions).parse(input);
|
|
605
607
|
if (options?.includeHeader === false) {
|
|
606
608
|
string = '\n' + string;
|
|
@@ -7,13 +7,26 @@ export type EmailOptions = SendMailOptions & {
|
|
|
7
7
|
data: Record<string, any>;
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
|
+
export type DefaultTemplateData = {
|
|
11
|
+
projectName: string;
|
|
12
|
+
projectColor: string;
|
|
13
|
+
projectLogo: string;
|
|
14
|
+
projectUrl: string;
|
|
15
|
+
};
|
|
10
16
|
export declare class MailService {
|
|
11
17
|
schema: SchemaOverview;
|
|
12
18
|
accountability: Accountability | null;
|
|
13
19
|
knex: Knex;
|
|
14
20
|
mailer: Transporter;
|
|
15
21
|
constructor(opts: AbstractServiceOptions);
|
|
16
|
-
send<T>(
|
|
22
|
+
send<T>(data: EmailOptions, options?: {
|
|
23
|
+
defaultTemplateData: DefaultTemplateData;
|
|
24
|
+
}): Promise<T | null>;
|
|
17
25
|
private renderTemplate;
|
|
18
|
-
|
|
26
|
+
getDefaultTemplateData(): Promise<{
|
|
27
|
+
projectName: any;
|
|
28
|
+
projectColor: any;
|
|
29
|
+
projectLogo: string;
|
|
30
|
+
projectUrl: any;
|
|
31
|
+
}>;
|
|
19
32
|
}
|
|
@@ -37,14 +37,15 @@ export class MailService {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
async send(options) {
|
|
40
|
+
async send(data, options) {
|
|
41
41
|
await useEmailRateLimiterQueue();
|
|
42
|
-
const payload = await emitter.emitFilter(`email.send`,
|
|
42
|
+
const payload = await emitter.emitFilter(`email.send`, data, {});
|
|
43
43
|
if (!payload)
|
|
44
44
|
return null;
|
|
45
45
|
const { template, ...emailOptions } = payload;
|
|
46
|
-
let { html } =
|
|
47
|
-
|
|
46
|
+
let { html } = data;
|
|
47
|
+
// option for providing tempalate data was added to prevent transaction race conditions with preceding promises
|
|
48
|
+
const defaultTemplateData = options?.defaultTemplateData ?? (await this.getDefaultTemplateData());
|
|
48
49
|
if (isObject(emailOptions.from) && (!emailOptions.from.name || !emailOptions.from.address)) {
|
|
49
50
|
throw new InvalidPayloadError({ reason: 'A name and address property are required in the "from" object' });
|
|
50
51
|
}
|
|
@@ -48,6 +48,8 @@ export class NotificationsService extends ItemsService {
|
|
|
48
48
|
},
|
|
49
49
|
to: user['email'],
|
|
50
50
|
subject: data.subject,
|
|
51
|
+
}, {
|
|
52
|
+
defaultTemplateData: await mailService.getDefaultTemplateData(),
|
|
51
53
|
})
|
|
52
54
|
.catch((error) => {
|
|
53
55
|
logger.error(error, `Could not send notification via mail`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TusDriver } from '@directus/storage';
|
|
2
2
|
import type { Accountability, File, SchemaOverview } from '@directus/types';
|
|
3
|
-
import stream from 'node:stream';
|
|
4
3
|
import { DataStore, Upload } from '@tus/utils';
|
|
4
|
+
import stream from 'node:stream';
|
|
5
5
|
export type TusDataStoreConfig = {
|
|
6
6
|
constants: {
|
|
7
7
|
ENABLED: boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import formatTitle from '@directus/format-title';
|
|
2
|
+
import { DataStore, ERRORS, Upload } from '@tus/utils';
|
|
3
|
+
import { omit } from 'lodash-es';
|
|
2
4
|
import { extension } from 'mime-types';
|
|
3
5
|
import { extname } from 'node:path';
|
|
4
6
|
import stream from 'node:stream';
|
|
5
|
-
import { DataStore, ERRORS, Upload } from '@tus/utils';
|
|
6
|
-
import { ItemsService } from '../items.js';
|
|
7
|
-
import { useLogger } from '../../logger/index.js';
|
|
8
7
|
import getDatabase from '../../database/index.js';
|
|
9
|
-
import {
|
|
8
|
+
import { useLogger } from '../../logger/index.js';
|
|
9
|
+
import { ItemsService } from '../items.js';
|
|
10
10
|
export class TusDataStore extends DataStore {
|
|
11
11
|
chunkSize;
|
|
12
12
|
maxSize;
|
|
@@ -66,7 +66,7 @@ export class TusDataStore extends DataStore {
|
|
|
66
66
|
upload.metadata['replace_id'] = upload.metadata['id'];
|
|
67
67
|
}
|
|
68
68
|
const fileData = {
|
|
69
|
-
...omit(upload.metadata, ['id']),
|
|
69
|
+
...omit(upload.metadata, ['id', 'replace_id']),
|
|
70
70
|
tus_id: upload.id,
|
|
71
71
|
tus_data: upload,
|
|
72
72
|
filesize: upload.size,
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SettingsService } from '../../services/settings.js';
|
|
2
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
2
3
|
export const getSettings = async (db) => {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const settingsService = new SettingsService({
|
|
5
|
+
knex: db,
|
|
6
|
+
schema: await getSchema({ database: db }),
|
|
7
|
+
});
|
|
8
|
+
const settings = (await settingsService.readSingleton({
|
|
9
|
+
fields: ['project_id', 'mcp_enabled', 'mcp_allow_deletes', 'mcp_system_prompt_enabled', 'visual_editor_urls'],
|
|
10
|
+
}));
|
|
7
11
|
return {
|
|
8
12
|
project_id: settings.project_id,
|
|
9
|
-
mcp_enabled:
|
|
10
|
-
mcp_allow_deletes:
|
|
11
|
-
mcp_system_prompt_enabled:
|
|
12
|
-
visual_editor_urls: settings.visual_editor_urls
|
|
13
|
+
mcp_enabled: settings?.mcp_enabled || false,
|
|
14
|
+
mcp_allow_deletes: settings?.mcp_allow_deletes || false,
|
|
15
|
+
mcp_system_prompt_enabled: settings?.mcp_system_prompt_enabled || false,
|
|
16
|
+
visual_editor_urls: settings.visual_editor_urls?.length || 0,
|
|
13
17
|
};
|
|
14
18
|
};
|
|
@@ -20,7 +20,7 @@ const querySchema = Joi.object({
|
|
|
20
20
|
page: Joi.number().integer().min(0),
|
|
21
21
|
meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
|
|
22
22
|
search: Joi.string(),
|
|
23
|
-
export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
|
|
23
|
+
export: Joi.string().valid('csv', 'csv_utf8', 'json', 'xml', 'yaml'),
|
|
24
24
|
version: Joi.string(),
|
|
25
25
|
versionRaw: Joi.boolean(),
|
|
26
26
|
aggregate: Joi.object(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.1.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
|
62
|
-
"@aws-sdk/client-sesv2": "3.
|
|
62
|
+
"@aws-sdk/client-sesv2": "3.928.0",
|
|
63
63
|
"@godaddy/terminus": "4.12.1",
|
|
64
|
-
"@modelcontextprotocol/sdk": "1.
|
|
64
|
+
"@modelcontextprotocol/sdk": "1.21.1",
|
|
65
65
|
"@rollup/plugin-alias": "5.1.1",
|
|
66
66
|
"@rollup/plugin-node-resolve": "16.0.3",
|
|
67
67
|
"@rollup/plugin-virtual": "3.0.2",
|
|
@@ -93,17 +93,17 @@
|
|
|
93
93
|
"flat": "6.0.1",
|
|
94
94
|
"fs-extra": "11.3.2",
|
|
95
95
|
"glob-to-regexp": "0.4.1",
|
|
96
|
-
"graphql": "16.
|
|
96
|
+
"graphql": "16.12.0",
|
|
97
97
|
"graphql-compose": "9.1.0",
|
|
98
98
|
"graphql-ws": "6.0.6",
|
|
99
99
|
"helmet": "8.1.0",
|
|
100
100
|
"icc": "3.0.0",
|
|
101
|
-
"inquirer": "12.
|
|
101
|
+
"inquirer": "12.11.0",
|
|
102
102
|
"ioredis": "5.8.2",
|
|
103
103
|
"ip-matching": "2.1.2",
|
|
104
104
|
"isolated-vm": "5.0.3",
|
|
105
105
|
"joi": "18.0.1",
|
|
106
|
-
"js-yaml": "4.1.
|
|
106
|
+
"js-yaml": "4.1.1",
|
|
107
107
|
"js2xmlparser": "5.0.0",
|
|
108
108
|
"json2csv": "5.0.7",
|
|
109
109
|
"jsonwebtoken": "9.0.2",
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"ms": "2.1.3",
|
|
121
121
|
"nanoid": "5.1.6",
|
|
122
122
|
"node-machine-id": "1.1.12",
|
|
123
|
-
"cron": "4.3.
|
|
123
|
+
"cron": "4.3.4",
|
|
124
124
|
"nodemailer": "7.0.10",
|
|
125
125
|
"object-hash": "3.0.0",
|
|
126
126
|
"openapi3-ts": "4.5.0",
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"rollup": "4.52.5",
|
|
144
144
|
"samlify": "2.10.1",
|
|
145
145
|
"sanitize-html": "2.17.0",
|
|
146
|
-
"sharp": "0.34.
|
|
146
|
+
"sharp": "0.34.5",
|
|
147
147
|
"snappy": "7.3.3",
|
|
148
148
|
"stream-json": "1.9.1",
|
|
149
149
|
"tar": "7.5.2",
|
|
@@ -153,30 +153,30 @@
|
|
|
153
153
|
"ws": "8.18.3",
|
|
154
154
|
"zod": "4.1.12",
|
|
155
155
|
"zod-validation-error": "4.0.2",
|
|
156
|
-
"@directus/
|
|
157
|
-
"@directus/
|
|
156
|
+
"@directus/app": "14.3.0",
|
|
157
|
+
"@directus/env": "5.3.2",
|
|
158
|
+
"@directus/extensions-registry": "3.0.14",
|
|
159
|
+
"@directus/extensions": "3.0.14",
|
|
158
160
|
"@directus/errors": "2.0.5",
|
|
159
|
-
"@directus/
|
|
160
|
-
"@directus/extensions": "
|
|
161
|
-
"@directus/
|
|
161
|
+
"@directus/constants": "14.0.0",
|
|
162
|
+
"@directus/extensions-sdk": "17.0.3",
|
|
163
|
+
"@directus/memory": "3.0.12",
|
|
162
164
|
"@directus/format-title": "12.1.1",
|
|
163
|
-
"@directus/
|
|
164
|
-
"@directus/
|
|
165
|
-
"@directus/
|
|
165
|
+
"@directus/pressure": "3.0.12",
|
|
166
|
+
"@directus/schema-builder": "0.0.9",
|
|
167
|
+
"@directus/specs": "11.2.0",
|
|
166
168
|
"@directus/schema": "13.0.4",
|
|
167
|
-
"@directus/specs": "11.1.1",
|
|
168
169
|
"@directus/storage": "12.0.3",
|
|
169
|
-
"@directus/storage-driver-
|
|
170
|
-
"@directus/
|
|
171
|
-
"@directus/storage-driver-cloudinary": "12.0.11",
|
|
172
|
-
"@directus/storage-driver-gcs": "12.0.11",
|
|
170
|
+
"@directus/storage-driver-cloudinary": "12.0.12",
|
|
171
|
+
"@directus/storage-driver-azure": "12.0.12",
|
|
173
172
|
"@directus/storage-driver-local": "12.0.3",
|
|
174
|
-
"@directus/storage-driver-
|
|
175
|
-
"@directus/storage-driver-
|
|
176
|
-
"@directus/
|
|
177
|
-
"@directus/utils": "13.0.
|
|
178
|
-
"@directus/validation": "2.0.
|
|
179
|
-
"directus": "
|
|
173
|
+
"@directus/storage-driver-gcs": "12.0.12",
|
|
174
|
+
"@directus/storage-driver-supabase": "3.0.12",
|
|
175
|
+
"@directus/storage-driver-s3": "12.0.12",
|
|
176
|
+
"@directus/utils": "13.0.13",
|
|
177
|
+
"@directus/validation": "2.0.12",
|
|
178
|
+
"@directus/system-data": "3.4.2",
|
|
179
|
+
"directus": "11.13.4"
|
|
180
180
|
},
|
|
181
181
|
"devDependencies": {
|
|
182
182
|
"@directus/tsconfig": "3.0.0",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"@types/node": "22.13.14",
|
|
206
206
|
"@types/nodemailer": "7.0.3",
|
|
207
207
|
"@types/object-hash": "3.0.6",
|
|
208
|
-
"@types/papaparse": "5.
|
|
208
|
+
"@types/papaparse": "5.5.0",
|
|
209
209
|
"@types/proxy-addr": "2.0.3",
|
|
210
210
|
"@types/qs": "6.14.0",
|
|
211
211
|
"@types/sanitize-html": "2.16.0",
|
|
@@ -219,8 +219,8 @@
|
|
|
219
219
|
"knex-mock-client": "3.0.2",
|
|
220
220
|
"typescript": "5.9.3",
|
|
221
221
|
"vitest": "3.2.4",
|
|
222
|
-
"@directus/schema-builder": "0.0.
|
|
223
|
-
"@directus/types": "13.
|
|
222
|
+
"@directus/schema-builder": "0.0.9",
|
|
223
|
+
"@directus/types": "13.4.0"
|
|
224
224
|
},
|
|
225
225
|
"optionalDependencies": {
|
|
226
226
|
"@keyv/redis": "3.0.1",
|