@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.
@@ -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
- const tarReadableStream = await download(versionId, env['MARKETPLACE_TRUST'] === 'sandbox', options);
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
- throw new ServiceUnavailableError({ service: 'marketplace', reason: 'Could not download and extract the extension' }, { cause: err });
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 Process:**
250
+ **Critical Workflow - Follow This Order:**
251
251
 
252
- 1. Create flow first to get flow ID
253
- 2. Use `operations` tool to add/manage operations
254
- 3. Operations execute in sequence based on resolve/reject paths
255
- 4. Link operations via UUIDs in resolve/reject fields </operations_integration>
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
- **See the `operations` tool for complete data chain syntax and examples.**
365
+ Operations can access data using `{{ variable }}` syntax:
323
366
 
324
- Operations can access:
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
- - `$trigger` - Initial trigger data
327
- - `$accountability` - User/permission context
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 - **ESSENTIAL READING**
149
+ ## Workflow Creation Process
150
150
 
151
- **⚠️ CRITICAL**: Follow this exact order or operations will fail
151
+ **Critical Order:** Create flow first Create operations with null resolve/reject → Link operations via UUIDs → Update
152
+ flow entry point.
152
153
 
153
- <workflow_steps>
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-Based Positioning - **ALWAYS SET POSITIONS**
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
- ```json
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
- **Read Items:**
162
+ **Patterns:** Linear (19,1)→(37,1)→(55,1). Branching: success (37,1), error (37,19). </positioning_system>
332
163
 
333
- ```json
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
- "type": "item-update",
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
- **Delete Items:**
171
+ **item-create/read/update/delete** - CRUD operations
363
172
 
364
173
  ```json
365
- {
366
- "type": "item-delete",
367
- "options": {
368
- "collection": "temp_data",
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
- </item_operations>
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 // Validate input\n if (!data.$trigger.payload.value) {\n throw new Error('Missing required value');\n }\n \n // Process data\n const result = data.$trigger.payload.value * 2;\n \n // Return must be valid JSON\n return {\n result: result,\n processed: true\n };\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
- **Common Use Cases**: Data transformation, calculations, complex logic, formatting, extracting nested values </exec>
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", "{{ $trigger.payload.email }}"],
197
+ "to": ["user@example.com"],
401
198
  "subject": "Order Confirmation",
402
- "type": "markdown", // "markdown" (default), "wysiwyg", or "template"
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
- **Template Mode:**
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
- </notification>
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
- **Real Example (Netlify Deploy Hook)**:
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", // "sign", "verify", or "decode"
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
- </json_web_token>
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
- **Trigger Flow:**
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
- **Log:**
261
+ **sleep/log/throw-error** - Utilities
566
262
 
567
263
  ```json
568
- {
569
- "type": "log",
570
- "options": { "message": "Processing item: {{ $trigger.payload.id }}" }
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
- **Throw Error:**
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
- **⚠️ Avoid `$last`:** While `{{ $last }}` references the previous operation's result, avoid using it in production
597
- flows. If you reorder operations, `$last` will reference a different operation, potentially breaking your flow. Always
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. **DO NOT** create operations without a flow - create flow first
677
- 2. **DO NOT** use operation keys in resolve/reject - use UUIDs (see <workflow_example> above)
678
- 3. **DO NOT** try to reference operations that do not exist yet
679
- 4. **DO NOT** use duplicate keys within the same flow
680
- 5. **DO NOT** create circular references in resolve/reject paths
681
- 6. **DO NOT** forget to handle both success and failure paths
682
- 7. **DO NOT** pass stringified JSON - use native objects (except request body)
683
- 8. **DO NOT** leave operations at default position (0,0) - see <positioning_system> above
684
- 9. **DO NOT** use dot notation in condition filters - see <critical_syntax> above
685
- 10. **DO NOT** use wrong format for request operations - see <critical_syntax> above </common_mistakes>
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
- <invalid_foreign_key>
689
- ### "Invalid foreign key" Errors
690
-
691
- This typically means you're trying to reference an operation that doesn't exist:
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): any;
3
- export declare function filterReplaceM2ADeep(deep_arg: NestedDeepQuery | null | undefined, collection: string, schema: SchemaOverview): any;
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 [field, any_collection] = key.split('__');
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 [field, any_collection] = key.split('__');
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 === 'csv') {
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 === 'csv') {
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>(options: EmailOptions): Promise<T | null>;
22
+ send<T>(data: EmailOptions, options?: {
23
+ defaultTemplateData: DefaultTemplateData;
24
+ }): Promise<T | null>;
17
25
  private renderTemplate;
18
- private getDefaultTemplateData;
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`, options, {});
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 } = options;
47
- const defaultTemplateData = await this.getDefaultTemplateData();
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 { omit } from 'lodash-es';
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 { toBoolean } from '@directus/utils';
1
+ import { SettingsService } from '../../services/settings.js';
2
+ import { getSchema } from '../../utils/get-schema.js';
2
3
  export const getSettings = async (db) => {
3
- const settings = await db
4
- .select('project_id', 'mcp_enabled', 'mcp_allow_deletes', 'mcp_system_prompt_enabled', 'visual_editor_urls')
5
- .from('directus_settings')
6
- .first();
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: toBoolean(settings?.mcp_enabled),
10
- mcp_allow_deletes: toBoolean(settings?.mcp_allow_deletes),
11
- mcp_system_prompt_enabled: toBoolean(settings?.mcp_system_prompt_enabled),
12
- visual_editor_urls: settings.visual_editor_urls ? JSON.parse(settings.visual_editor_urls).length : 0,
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.0.2",
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.918.0",
62
+ "@aws-sdk/client-sesv2": "3.928.0",
63
63
  "@godaddy/terminus": "4.12.1",
64
- "@modelcontextprotocol/sdk": "1.20.2",
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.11.0",
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.10.0",
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.0",
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.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.4",
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/env": "5.3.1",
157
- "@directus/constants": "14.0.0",
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/app": "14.1.2",
160
- "@directus/extensions": "3.0.13",
161
- "@directus/extensions-sdk": "17.0.2",
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/memory": "3.0.11",
164
- "@directus/extensions-registry": "3.0.13",
165
- "@directus/pressure": "3.0.11",
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-azure": "12.0.11",
170
- "@directus/schema-builder": "0.0.8",
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-supabase": "3.0.11",
175
- "@directus/storage-driver-s3": "12.0.11",
176
- "@directus/system-data": "3.4.1",
177
- "@directus/utils": "13.0.12",
178
- "@directus/validation": "2.0.11",
179
- "directus": "11.13.2"
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.3.16",
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.8",
223
- "@directus/types": "13.3.1"
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",