@directus/api 33.3.1 → 34.0.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/ai/chat/lib/create-ui-stream.js +2 -1
- package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
- package/dist/ai/chat/lib/transform-file-parts.js +36 -0
- package/dist/ai/files/adapters/anthropic.d.ts +3 -0
- package/dist/ai/files/adapters/anthropic.js +25 -0
- package/dist/ai/files/adapters/google.d.ts +3 -0
- package/dist/ai/files/adapters/google.js +58 -0
- package/dist/ai/files/adapters/index.d.ts +3 -0
- package/dist/ai/files/adapters/index.js +3 -0
- package/dist/ai/files/adapters/openai.d.ts +3 -0
- package/dist/ai/files/adapters/openai.js +22 -0
- package/dist/ai/files/controllers/upload.d.ts +2 -0
- package/dist/ai/files/controllers/upload.js +101 -0
- package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
- package/dist/ai/files/lib/fetch-provider.js +23 -0
- package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
- package/dist/ai/files/lib/upload-to-provider.js +26 -0
- package/dist/ai/files/router.d.ts +1 -0
- package/dist/ai/files/router.js +5 -0
- package/dist/ai/files/types.d.ts +5 -0
- package/dist/ai/files/types.js +1 -0
- package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
- package/dist/ai/providers/anthropic-file-support.js +94 -0
- package/dist/ai/providers/registry.js +3 -6
- package/dist/ai/tools/fields/index.d.ts +3 -3
- package/dist/ai/tools/fields/index.js +9 -3
- package/dist/ai/tools/flows/index.d.ts +16 -16
- package/dist/ai/tools/schema.d.ts +8 -8
- package/dist/ai/tools/schema.js +2 -2
- package/dist/app.js +10 -1
- package/dist/auth/drivers/oauth2.js +10 -4
- package/dist/auth/drivers/openid.js +10 -4
- package/dist/auth/drivers/saml.js +20 -10
- package/dist/auth/utils/resolve-login-redirect.d.ts +11 -0
- package/dist/auth/utils/resolve-login-redirect.js +62 -0
- package/dist/controllers/deployment-webhooks.d.ts +2 -0
- package/dist/controllers/deployment-webhooks.js +95 -0
- package/dist/controllers/deployment.js +61 -165
- package/dist/controllers/files.js +2 -1
- package/dist/controllers/server.js +32 -26
- package/dist/controllers/tus.js +33 -2
- package/dist/controllers/utils.js +18 -0
- package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
- package/dist/database/helpers/date/dialects/oracle.js +2 -0
- package/dist/database/helpers/date/dialects/sqlite.js +2 -0
- package/dist/database/helpers/date/types.d.ts +1 -1
- package/dist/database/helpers/date/types.js +3 -1
- package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/mssql.js +21 -0
- package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
- package/dist/database/helpers/fn/dialects/mysql.js +30 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/oracle.js +21 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
- package/dist/database/helpers/fn/dialects/postgres.js +40 -0
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
- package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
- package/dist/database/helpers/fn/json/parse-function.js +66 -0
- package/dist/database/helpers/fn/types.d.ts +8 -0
- package/dist/database/helpers/fn/types.js +19 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +11 -0
- package/dist/database/helpers/schema/types.d.ts +1 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
- package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
- package/dist/database/run-ast/lib/apply-query/aggregate.js +4 -4
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +17 -7
- package/dist/database/run-ast/lib/parse-current-level.js +8 -1
- package/dist/database/run-ast/run-ast.js +11 -1
- package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
- package/dist/database/run-ast/utils/get-column.js +13 -2
- package/dist/deployment/deployment.d.ts +25 -2
- package/dist/deployment/drivers/netlify.d.ts +6 -2
- package/dist/deployment/drivers/netlify.js +114 -12
- package/dist/deployment/drivers/vercel.d.ts +5 -2
- package/dist/deployment/drivers/vercel.js +84 -5
- package/dist/deployment.d.ts +5 -0
- package/dist/deployment.js +34 -0
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +1 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
- package/dist/request/is-denied-ip.js +24 -23
- package/dist/services/authentication.js +27 -22
- package/dist/services/collections.js +1 -0
- package/dist/services/deployment-projects.d.ts +31 -2
- package/dist/services/deployment-projects.js +109 -5
- package/dist/services/deployment-runs.d.ts +19 -1
- package/dist/services/deployment-runs.js +86 -0
- package/dist/services/deployment.d.ts +44 -3
- package/dist/services/deployment.js +263 -15
- package/dist/services/files/utils/get-metadata.js +6 -6
- package/dist/services/files.d.ts +3 -1
- package/dist/services/files.js +26 -3
- package/dist/services/graphql/resolvers/query.js +23 -6
- package/dist/services/graphql/resolvers/system.js +35 -27
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +1 -1
- package/dist/services/users.js +6 -1
- package/dist/test-utils/README.md +112 -0
- package/dist/test-utils/controllers.d.ts +65 -0
- package/dist/test-utils/controllers.js +100 -0
- package/dist/test-utils/database.d.ts +1 -1
- package/dist/test-utils/database.js +3 -1
- package/dist/utils/get-field-relational-depth.d.ts +13 -0
- package/dist/utils/get-field-relational-depth.js +22 -0
- package/dist/utils/parse-value.d.ts +4 -0
- package/dist/utils/parse-value.js +11 -0
- package/dist/utils/sanitize-query.js +3 -2
- package/dist/utils/split-fields.d.ts +4 -0
- package/dist/utils/split-fields.js +32 -0
- package/dist/utils/validate-query.js +2 -1
- package/package.json +36 -36
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -7
- package/dist/auth/utils/is-login-redirect-allowed.js +0 -39
|
@@ -17,6 +17,7 @@ This directory contains mock implementations for commonly used modules in servic
|
|
|
17
17
|
- **[files-service.ts](#files-servicets)** - FilesService mocks
|
|
18
18
|
- **[folders-service.ts](#folders-servicets)** - FoldersService mocks
|
|
19
19
|
- **[test-helpers.ts](#test-helpersts)** - Test data factory functions
|
|
20
|
+
- **[controllers.ts](#controllersts)** - Controller/router testing helpers
|
|
20
21
|
|
|
21
22
|
## Quick Start
|
|
22
23
|
|
|
@@ -521,6 +522,116 @@ const buildTreeSpy = vi.spyOn(FoldersService.prototype, 'buildTree').mockResolve
|
|
|
521
522
|
|
|
522
523
|
---
|
|
523
524
|
|
|
525
|
+
### controllers.ts
|
|
526
|
+
|
|
527
|
+
Provides helpers for extracting route handlers from Express routers and creating mock Express request/response objects
|
|
528
|
+
for controller tests.
|
|
529
|
+
|
|
530
|
+
#### `getRouteHandler(router, method, path)`
|
|
531
|
+
|
|
532
|
+
Extracts the middleware/handler stack for a specific route from an Express router.
|
|
533
|
+
|
|
534
|
+
**Parameters:**
|
|
535
|
+
|
|
536
|
+
- `router`: The Express `Router` instance
|
|
537
|
+
- `method`: HTTP method (`'GET'`, `'POST'`, `'PATCH'`, `'DELETE'`, etc.)
|
|
538
|
+
- `path`: Route path (e.g. `'/'`, `'/:id'`)
|
|
539
|
+
|
|
540
|
+
**Returns:** Array of `{ handle: (...args) => any }` layers for the matched route
|
|
541
|
+
|
|
542
|
+
**Throws:** If no matching route is found
|
|
543
|
+
|
|
544
|
+
**Example:**
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { default as router } from './tus.js';
|
|
548
|
+
import { getRouteHandler } from '../test-utils/controllers.js';
|
|
549
|
+
|
|
550
|
+
const [checkAccess, handler] = getRouteHandler(router, 'POST', '/');
|
|
551
|
+
await checkAccess?.handle(req, res, next);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
#### `createMockRequest(overrides?)`
|
|
555
|
+
|
|
556
|
+
Creates a mock Express Request pre-populated with common Directus properties (`accountability`, `schema`,
|
|
557
|
+
`sanitizedQuery`, etc.).
|
|
558
|
+
|
|
559
|
+
**Parameters:**
|
|
560
|
+
|
|
561
|
+
- `overrides` (optional): Properties to merge into the mock request
|
|
562
|
+
|
|
563
|
+
**Returns:** Mock `Request` object
|
|
564
|
+
|
|
565
|
+
**Example:**
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { createMockRequest } from '../test-utils/controllers.js';
|
|
569
|
+
|
|
570
|
+
// Minimal request
|
|
571
|
+
const req = createMockRequest({ schema });
|
|
572
|
+
|
|
573
|
+
// With accountability and custom header
|
|
574
|
+
const req = createMockRequest({
|
|
575
|
+
method: 'POST',
|
|
576
|
+
accountability,
|
|
577
|
+
schema,
|
|
578
|
+
header: vi.fn().mockReturnValue('some-value'),
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### `createMockResponse(overrides?)`
|
|
583
|
+
|
|
584
|
+
Creates a mock Express Response with chainable methods (`status`, `json`, `send`, `set`, `end`).
|
|
585
|
+
|
|
586
|
+
**Parameters:**
|
|
587
|
+
|
|
588
|
+
- `overrides` (optional): Properties to merge into the mock response
|
|
589
|
+
|
|
590
|
+
**Returns:** Mock `Response` object
|
|
591
|
+
|
|
592
|
+
**Example:**
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
import { createMockResponse } from '../test-utils/controllers.js';
|
|
596
|
+
|
|
597
|
+
const res = createMockResponse();
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### Full Controller Test Example
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
import { SchemaBuilder } from '@directus/schema-builder';
|
|
604
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
605
|
+
import { createMockRequest, createMockResponse, getRouteHandler } from '../test-utils/controllers.js';
|
|
606
|
+
import { default as router } from './controller.js';
|
|
607
|
+
|
|
608
|
+
const schema = new SchemaBuilder()
|
|
609
|
+
.collection('collection', (c) => {
|
|
610
|
+
c.field('id').integer().primary();
|
|
611
|
+
c.field('title').string();
|
|
612
|
+
})
|
|
613
|
+
.build();
|
|
614
|
+
|
|
615
|
+
describe('controller', () => {
|
|
616
|
+
beforeEach(() => {
|
|
617
|
+
vi.clearAllMocks();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('validates access on POST', async () => {
|
|
621
|
+
const req = createMockRequest({ method: 'POST', accountability, schema });
|
|
622
|
+
const res = createMockResponse();
|
|
623
|
+
const next = vi.fn();
|
|
624
|
+
|
|
625
|
+
const [firstHandler] = getRouteHandler(router, 'POST', '/');
|
|
626
|
+
await firstHandler?.handle(req, res, next);
|
|
627
|
+
|
|
628
|
+
expect(next).toHaveBeenCalled();
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
524
635
|
## Common Patterns
|
|
525
636
|
|
|
526
637
|
### Full Service Test Setup
|
|
@@ -737,6 +848,7 @@ See these files for complete examples:
|
|
|
737
848
|
|
|
738
849
|
- [collections.test.ts](../services/collections.test.ts) - Full service test with schema operations
|
|
739
850
|
- [fields.test.ts](../services/fields.test.ts) - Complex service test with field management
|
|
851
|
+
- [tus.test.ts](../controllers/tus.test.ts) - Controller test with route handler extraction and access validation
|
|
740
852
|
|
|
741
853
|
---
|
|
742
854
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller testing utilities
|
|
3
|
+
* Provides helpers for extracting route handlers and creating mock Express request/response objects
|
|
4
|
+
*/
|
|
5
|
+
import type { Request, Response, Router } from 'express';
|
|
6
|
+
/**
|
|
7
|
+
* Get a route handler stack from an Express router
|
|
8
|
+
*
|
|
9
|
+
* @param router The Express router instance to search
|
|
10
|
+
* @param method HTTP method (GET, POST, PATCH, DELETE, etc.)
|
|
11
|
+
* @param path Route path (e.g. '/', '/:id')
|
|
12
|
+
* @returns Array of middleware/handler layers for the matched route
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { default as router } from './controller.js';
|
|
17
|
+
|
|
18
|
+
* const [firstHandler, secondHandler] = getRouteHandler(router, 'POST', '/');
|
|
19
|
+
* await firstHandler?.handle(req, res, next);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function getRouteHandler(router: Router, method: string, path: string): Array<{
|
|
23
|
+
handle: (...args: any[]) => any;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a mock Express Request with common Directus properties.
|
|
27
|
+
*
|
|
28
|
+
* @param overrides Properties to merge into the mock request
|
|
29
|
+
* @returns Mock Express Request object
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Basic usage
|
|
34
|
+
* const req = createMockRequest({ method: 'POST', accountability });
|
|
35
|
+
*
|
|
36
|
+
* // With custom schema
|
|
37
|
+
* const req = createMockRequest({
|
|
38
|
+
* method: 'PATCH',
|
|
39
|
+
* params: { id: 'file-1' },
|
|
40
|
+
* schema: mySchema,
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // With custom header mock
|
|
44
|
+
* const req = createMockRequest({
|
|
45
|
+
* header: vi.fn().mockReturnValue('some-header-value'),
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function createMockRequest(overrides?: Partial<Request>): Request;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a mock Express Response with chainable methods.
|
|
52
|
+
*
|
|
53
|
+
* @param overrides Properties to merge into the mock response
|
|
54
|
+
* @returns Mock Express Response object
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Basic usage
|
|
59
|
+
* const res = createMockResponse();
|
|
60
|
+
*
|
|
61
|
+
* // With custom locals
|
|
62
|
+
* const res = createMockResponse({ locals: { payload: { data: [] } } });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function createMockResponse(overrides?: Partial<Response>): Response;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller testing utilities
|
|
3
|
+
* Provides helpers for extracting route handlers and creating mock Express request/response objects
|
|
4
|
+
*/
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
/**
|
|
7
|
+
* Get a route handler stack from an Express router
|
|
8
|
+
*
|
|
9
|
+
* @param router The Express router instance to search
|
|
10
|
+
* @param method HTTP method (GET, POST, PATCH, DELETE, etc.)
|
|
11
|
+
* @param path Route path (e.g. '/', '/:id')
|
|
12
|
+
* @returns Array of middleware/handler layers for the matched route
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { default as router } from './controller.js';
|
|
17
|
+
|
|
18
|
+
* const [firstHandler, secondHandler] = getRouteHandler(router, 'POST', '/');
|
|
19
|
+
* await firstHandler?.handle(req, res, next);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function getRouteHandler(router, method, path) {
|
|
23
|
+
const stack = router.stack;
|
|
24
|
+
const layer = stack.find((l) => l.route?.path === path && l.route?.methods[method.toLowerCase()]);
|
|
25
|
+
if (!layer)
|
|
26
|
+
throw new Error(`No route found for ${method} ${path}`);
|
|
27
|
+
return layer.route.stack;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a mock Express Request with common Directus properties.
|
|
31
|
+
*
|
|
32
|
+
* @param overrides Properties to merge into the mock request
|
|
33
|
+
* @returns Mock Express Request object
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Basic usage
|
|
38
|
+
* const req = createMockRequest({ method: 'POST', accountability });
|
|
39
|
+
*
|
|
40
|
+
* // With custom schema
|
|
41
|
+
* const req = createMockRequest({
|
|
42
|
+
* method: 'PATCH',
|
|
43
|
+
* params: { id: 'file-1' },
|
|
44
|
+
* schema: mySchema,
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // With custom header mock
|
|
48
|
+
* const req = createMockRequest({
|
|
49
|
+
* header: vi.fn().mockReturnValue('some-header-value'),
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createMockRequest(overrides = {}) {
|
|
54
|
+
const headerFn = vi.fn().mockReturnValue(undefined);
|
|
55
|
+
return {
|
|
56
|
+
method: 'GET',
|
|
57
|
+
headers: {},
|
|
58
|
+
params: {},
|
|
59
|
+
body: {},
|
|
60
|
+
header: headerFn,
|
|
61
|
+
get: headerFn,
|
|
62
|
+
is: vi.fn().mockReturnValue(false),
|
|
63
|
+
token: null,
|
|
64
|
+
collection: '',
|
|
65
|
+
singleton: false,
|
|
66
|
+
accountability: undefined,
|
|
67
|
+
sanitizedQuery: {},
|
|
68
|
+
schema: {
|
|
69
|
+
collections: {},
|
|
70
|
+
relations: [],
|
|
71
|
+
},
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a mock Express Response with chainable methods.
|
|
77
|
+
*
|
|
78
|
+
* @param overrides Properties to merge into the mock response
|
|
79
|
+
* @returns Mock Express Response object
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Basic usage
|
|
84
|
+
* const res = createMockResponse();
|
|
85
|
+
*
|
|
86
|
+
* // With custom locals
|
|
87
|
+
* const res = createMockResponse({ locals: { payload: { data: [] } } });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function createMockResponse(overrides = {}) {
|
|
91
|
+
return {
|
|
92
|
+
locals: {},
|
|
93
|
+
status: vi.fn().mockReturnThis(),
|
|
94
|
+
json: vi.fn().mockReturnThis(),
|
|
95
|
+
send: vi.fn().mockReturnThis(),
|
|
96
|
+
set: vi.fn().mockReturnThis(),
|
|
97
|
+
end: vi.fn().mockReturnThis(),
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -24,7 +24,7 @@ import type { DatabaseClient } from '@directus/types';
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
export declare function mockDatabase(client?: DatabaseClient): {
|
|
27
|
-
default: import("vitest").Mock<(
|
|
27
|
+
default: import("vitest").Mock<() => import("vitest").MockedFunction<import("knex").Knex<any, unknown[]>>>;
|
|
28
28
|
getDatabaseClient: import("vitest").Mock<(...args: any[]) => any>;
|
|
29
29
|
getSchemaInspector: import("vitest").Mock<(...args: any[]) => any>;
|
|
30
30
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Provides simplified mocks for src/database/index module used in service testing
|
|
4
4
|
*/
|
|
5
5
|
import { vi } from 'vitest';
|
|
6
|
+
import { createMockKnex } from './knex.js';
|
|
6
7
|
/**
|
|
7
8
|
* Creates a standard database mock for service tests
|
|
8
9
|
* This matches the pattern used across all service test files
|
|
@@ -24,8 +25,9 @@ import { vi } from 'vitest';
|
|
|
24
25
|
* ```
|
|
25
26
|
*/
|
|
26
27
|
export function mockDatabase(client = 'postgres') {
|
|
28
|
+
const { db } = createMockKnex();
|
|
27
29
|
return {
|
|
28
|
-
default: vi.fn(),
|
|
30
|
+
default: vi.fn(() => db),
|
|
29
31
|
getDatabaseClient: vi.fn().mockReturnValue(client),
|
|
30
32
|
getSchemaInspector: vi.fn(),
|
|
31
33
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the number of relational segments in a field path. Handles function syntax
|
|
3
|
+
* (e.g. json(), year()) by counting relational segments in the prefix and in the first argument
|
|
4
|
+
* separately, while ignoring subsequent arguments (e.g. json paths).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* getFieldRelationalDepth('a.b.c') // 3
|
|
8
|
+
* getFieldRelationalDepth('year(user.date_created)') // 2
|
|
9
|
+
* getFieldRelationalDepth('category_id.json(metadata, a.b.c)') // 2
|
|
10
|
+
* getFieldRelationalDepth('json(a.b.field, some.path)') // 3
|
|
11
|
+
* getFieldRelationalDepth('json(metadata, path.to.value)') // 1
|
|
12
|
+
*/
|
|
13
|
+
export declare function getFieldRelationalDepth(field: string): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the number of relational segments in a field path. Handles function syntax
|
|
3
|
+
* (e.g. json(), year()) by counting relational segments in the prefix and in the first argument
|
|
4
|
+
* separately, while ignoring subsequent arguments (e.g. json paths).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* getFieldRelationalDepth('a.b.c') // 3
|
|
8
|
+
* getFieldRelationalDepth('year(user.date_created)') // 2
|
|
9
|
+
* getFieldRelationalDepth('category_id.json(metadata, a.b.c)') // 2
|
|
10
|
+
* getFieldRelationalDepth('json(a.b.field, some.path)') // 3
|
|
11
|
+
* getFieldRelationalDepth('json(metadata, path.to.value)') // 1
|
|
12
|
+
*/
|
|
13
|
+
export function getFieldRelationalDepth(field) {
|
|
14
|
+
const openParenIndex = field.indexOf('(');
|
|
15
|
+
if (openParenIndex === -1) {
|
|
16
|
+
return field.split('.').length;
|
|
17
|
+
}
|
|
18
|
+
const functionDepth = field.slice(0, openParenIndex).split('.').length - 1;
|
|
19
|
+
const commaIndex = field.indexOf(',', openParenIndex);
|
|
20
|
+
const fieldDepth = field.slice(openParenIndex, commaIndex).split('.').length;
|
|
21
|
+
return functionDepth + fieldDepth;
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseJSON } from '@directus/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a value that might be a JSON string, returning a typed result or fallback.
|
|
4
|
+
*/
|
|
5
|
+
export function parseValue(value, fallback) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return fallback;
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
return parseJSON(value);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
@@ -9,6 +9,7 @@ import { contextHasDynamicVariables } from '../permissions/modules/process-ast/u
|
|
|
9
9
|
import { extractRequiredDynamicVariableContext } from '../permissions/utils/extract-required-dynamic-variable-context.js';
|
|
10
10
|
import { fetchDynamicVariableData } from '../permissions/utils/fetch-dynamic-variable-data.js';
|
|
11
11
|
import { Meta } from '../types/index.js';
|
|
12
|
+
import { splitFields } from './split-fields.js';
|
|
12
13
|
/**
|
|
13
14
|
* Sanitize the query parameters and parse them where necessary.
|
|
14
15
|
*/
|
|
@@ -81,7 +82,7 @@ function sanitizeFields(rawFields) {
|
|
|
81
82
|
return null;
|
|
82
83
|
let fields = [];
|
|
83
84
|
if (typeof rawFields === 'string') {
|
|
84
|
-
fields = rawFields
|
|
85
|
+
fields = splitFields(rawFields);
|
|
85
86
|
}
|
|
86
87
|
else if (Array.isArray(rawFields)) {
|
|
87
88
|
fields = rawFields;
|
|
@@ -90,7 +91,7 @@ function sanitizeFields(rawFields) {
|
|
|
90
91
|
throw new InvalidQueryError({ reason: '"fields" must be a string or array' });
|
|
91
92
|
}
|
|
92
93
|
// Case where array item includes CSV (fe fields[]=id,name):
|
|
93
|
-
fields = flatten(fields.map((field) => (field.includes(',') ? field
|
|
94
|
+
fields = flatten(fields.map((field) => (field.includes(',') ? splitFields(field) : field)));
|
|
94
95
|
fields = fields.map((field) => field.trim());
|
|
95
96
|
return fields;
|
|
96
97
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
+
/**
|
|
3
|
+
* Parenthesis aware splitting of fields allowing for `json(a, b)` field functions
|
|
4
|
+
*/
|
|
5
|
+
export function splitFields(input) {
|
|
6
|
+
const fields = [];
|
|
7
|
+
let current = '';
|
|
8
|
+
let depth = 0;
|
|
9
|
+
for (const char of input) {
|
|
10
|
+
if (char === '(') {
|
|
11
|
+
depth++;
|
|
12
|
+
if (depth > 1) {
|
|
13
|
+
throw new InvalidQueryError({ reason: 'Nested functions are not supported in "fields"' });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else if (char === ')') {
|
|
17
|
+
depth--;
|
|
18
|
+
}
|
|
19
|
+
if (char === ',' && depth === 0) {
|
|
20
|
+
fields.push(current);
|
|
21
|
+
current = '';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
current += char;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (depth !== 0) {
|
|
28
|
+
throw new InvalidQueryError({ reason: 'Missing closing parenthesis in "fields"' });
|
|
29
|
+
}
|
|
30
|
+
fields.push(current);
|
|
31
|
+
return fields;
|
|
32
|
+
}
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import { isPlainObject, uniq } from 'lodash-es';
|
|
5
5
|
import { stringify } from 'wellknown';
|
|
6
6
|
import { calculateFieldDepth } from './calculate-field-depth.js';
|
|
7
|
+
import { getFieldRelationalDepth } from './get-field-relational-depth.js';
|
|
7
8
|
const env = useEnv();
|
|
8
9
|
const querySchema = Joi.object({
|
|
9
10
|
fields: Joi.array().items(Joi.string()),
|
|
@@ -186,7 +187,7 @@ function validateRelationalDepth(query) {
|
|
|
186
187
|
}
|
|
187
188
|
fields = uniq(fields);
|
|
188
189
|
for (const field of fields) {
|
|
189
|
-
if (field
|
|
190
|
+
if (getFieldRelationalDepth(field) > maxRelationalDepth) {
|
|
190
191
|
throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
|
|
191
192
|
}
|
|
192
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "34.0.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"dist"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@ai-sdk/anthropic": "3.0.
|
|
62
|
-
"@ai-sdk/google": "3.0.
|
|
63
|
-
"@ai-sdk/openai": "3.0.
|
|
64
|
-
"@ai-sdk/openai-compatible": "2.0.
|
|
61
|
+
"@ai-sdk/anthropic": "3.0.58",
|
|
62
|
+
"@ai-sdk/google": "3.0.43",
|
|
63
|
+
"@ai-sdk/openai": "3.0.41",
|
|
64
|
+
"@ai-sdk/openai-compatible": "2.0.35",
|
|
65
65
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
|
66
66
|
"@aws-sdk/client-sesv2": "3.928.0",
|
|
67
67
|
"@godaddy/terminus": "4.12.1",
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"@rollup/plugin-virtual": "3.0.2",
|
|
73
73
|
"@tus/server": "2.3.0",
|
|
74
74
|
"@tus/utils": "0.6.0",
|
|
75
|
-
"ai": "6.0.
|
|
75
|
+
"ai": "6.0.116",
|
|
76
76
|
"archiver": "7.0.1",
|
|
77
77
|
"argon2": "0.44.0",
|
|
78
78
|
"async": "3.2.6",
|
|
79
79
|
"async-mutex": "0.5.0",
|
|
80
|
-
"axios": "1.
|
|
80
|
+
"axios": "1.13.5",
|
|
81
81
|
"busboy": "1.6.0",
|
|
82
82
|
"bytes": "3.1.2",
|
|
83
83
|
"camelcase": "8.0.0",
|
|
@@ -118,12 +118,12 @@
|
|
|
118
118
|
"keyv": "5.5.3",
|
|
119
119
|
"knex": "3.1.0",
|
|
120
120
|
"ldapts": "8.1.3",
|
|
121
|
-
"liquidjs": "10.
|
|
122
|
-
"lodash-es": "4.17.
|
|
121
|
+
"liquidjs": "10.25.0",
|
|
122
|
+
"lodash-es": "4.17.23",
|
|
123
123
|
"marked": "16.4.1",
|
|
124
124
|
"micromustache": "8.0.3",
|
|
125
125
|
"mime-types": "3.0.1",
|
|
126
|
-
"minimatch": "10.
|
|
126
|
+
"minimatch": "10.2.3",
|
|
127
127
|
"mnemonist": "0.40.3",
|
|
128
128
|
"ms": "2.1.3",
|
|
129
129
|
"nanoid": "5.1.6",
|
|
@@ -144,48 +144,48 @@
|
|
|
144
144
|
"pm2": "6.0.14",
|
|
145
145
|
"prom-client": "15.1.3",
|
|
146
146
|
"proxy-addr": "2.0.7",
|
|
147
|
-
"qs": "6.14.
|
|
147
|
+
"qs": "6.14.2",
|
|
148
148
|
"rate-limiter-flexible": "7.2.0",
|
|
149
149
|
"rolldown": "1.0.0-beta.31",
|
|
150
|
-
"rollup": "4.
|
|
150
|
+
"rollup": "4.59.0",
|
|
151
151
|
"samlify": "2.10.2",
|
|
152
152
|
"sanitize-filename": "1.6.3",
|
|
153
153
|
"sanitize-html": "2.17.0",
|
|
154
154
|
"sharp": "0.34.5",
|
|
155
155
|
"snappy": "7.3.3",
|
|
156
156
|
"stream-json": "1.9.1",
|
|
157
|
-
"tar": "7.5.
|
|
157
|
+
"tar": "7.5.8",
|
|
158
158
|
"tsx": "4.20.6",
|
|
159
159
|
"uuid": "11.1.0",
|
|
160
160
|
"wellknown": "0.5.0",
|
|
161
161
|
"ws": "8.18.3",
|
|
162
162
|
"zod": "4.1.12",
|
|
163
163
|
"zod-validation-error": "4.0.2",
|
|
164
|
-
"@directus/ai": "1.
|
|
165
|
-
"@directus/
|
|
166
|
-
"@directus/
|
|
167
|
-
"@directus/env": "5.
|
|
164
|
+
"@directus/ai": "1.3.0",
|
|
165
|
+
"@directus/constants": "14.2.0",
|
|
166
|
+
"@directus/app": "15.5.1",
|
|
167
|
+
"@directus/env": "5.6.1",
|
|
168
168
|
"@directus/errors": "2.2.0",
|
|
169
|
-
"@directus/extensions": "3.0.
|
|
170
|
-
"@directus/extensions-registry": "3.0.
|
|
171
|
-
"@directus/extensions-sdk": "17.0.9",
|
|
172
|
-
"@directus/memory": "3.1.2",
|
|
173
|
-
"@directus/schema": "13.0.5",
|
|
169
|
+
"@directus/extensions": "3.0.21",
|
|
170
|
+
"@directus/extensions-registry": "3.0.21",
|
|
174
171
|
"@directus/format-title": "12.1.1",
|
|
175
|
-
"@directus/
|
|
176
|
-
"@directus/
|
|
177
|
-
"@directus/
|
|
172
|
+
"@directus/memory": "3.1.4",
|
|
173
|
+
"@directus/pressure": "3.0.19",
|
|
174
|
+
"@directus/schema": "13.0.5",
|
|
175
|
+
"@directus/schema-builder": "0.0.16",
|
|
176
|
+
"@directus/extensions-sdk": "17.0.11",
|
|
177
|
+
"@directus/specs": "12.0.1",
|
|
178
178
|
"@directus/storage": "12.0.3",
|
|
179
|
-
"@directus/storage-driver-
|
|
180
|
-
"@directus/storage-driver-cloudinary": "12.0.
|
|
181
|
-
"@directus/storage-driver-s3": "12.1.
|
|
179
|
+
"@directus/storage-driver-gcs": "12.0.19",
|
|
180
|
+
"@directus/storage-driver-cloudinary": "12.0.19",
|
|
181
|
+
"@directus/storage-driver-s3": "12.1.5",
|
|
182
182
|
"@directus/storage-driver-local": "12.0.3",
|
|
183
|
-
"@directus/
|
|
184
|
-
"@directus/
|
|
185
|
-
"@directus/
|
|
186
|
-
"@directus/utils": "13.
|
|
187
|
-
"directus": "11.
|
|
188
|
-
"@directus/
|
|
183
|
+
"@directus/system-data": "4.3.0",
|
|
184
|
+
"@directus/storage-driver-supabase": "3.0.19",
|
|
185
|
+
"@directus/validation": "2.0.19",
|
|
186
|
+
"@directus/utils": "13.3.1",
|
|
187
|
+
"directus": "11.16.1",
|
|
188
|
+
"@directus/storage-driver-azure": "12.0.19"
|
|
189
189
|
},
|
|
190
190
|
"devDependencies": {
|
|
191
191
|
"@directus/tsconfig": "3.0.0",
|
|
@@ -228,8 +228,8 @@
|
|
|
228
228
|
"knex-mock-client": "3.0.2",
|
|
229
229
|
"typescript": "5.9.3",
|
|
230
230
|
"vitest": "3.2.4",
|
|
231
|
-
"@directus/schema-builder": "0.0.
|
|
232
|
-
"@directus/types": "14.
|
|
231
|
+
"@directus/schema-builder": "0.0.16",
|
|
232
|
+
"@directus/types": "14.3.1"
|
|
233
233
|
},
|
|
234
234
|
"optionalDependencies": {
|
|
235
235
|
"@keyv/redis": "3.0.1",
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if the defined redirect after successful SSO login is in the allow list
|
|
3
|
-
* @param provider SSO provider name
|
|
4
|
-
* @param redirect URL to redirect to after login
|
|
5
|
-
* @returns True if the redirect is allowed, false otherwise
|
|
6
|
-
*/
|
|
7
|
-
export declare function isLoginRedirectAllowed(provider: string, redirect: unknown): boolean;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useEnv } from '@directus/env';
|
|
2
|
-
import { toArray } from '@directus/utils';
|
|
3
|
-
import { useLogger } from '../../logger/index.js';
|
|
4
|
-
import isUrlAllowed from '../../utils/is-url-allowed.js';
|
|
5
|
-
/**
|
|
6
|
-
* Checks if the defined redirect after successful SSO login is in the allow list
|
|
7
|
-
* @param provider SSO provider name
|
|
8
|
-
* @param redirect URL to redirect to after login
|
|
9
|
-
* @returns True if the redirect is allowed, false otherwise
|
|
10
|
-
*/
|
|
11
|
-
export function isLoginRedirectAllowed(provider, redirect) {
|
|
12
|
-
if (!redirect)
|
|
13
|
-
return true; // empty redirect
|
|
14
|
-
if (typeof redirect !== 'string')
|
|
15
|
-
return false; // invalid type
|
|
16
|
-
const env = useEnv();
|
|
17
|
-
const publicUrl = env['PUBLIC_URL'];
|
|
18
|
-
if (!URL.canParse(redirect)) {
|
|
19
|
-
if (!redirect.startsWith('//')) {
|
|
20
|
-
// should be a relative path like `/admin/test`
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
// domain without protocol `//example.com/test`
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
|
|
27
|
-
if (envKey in env) {
|
|
28
|
-
if (isUrlAllowed(redirect, [...toArray(env[envKey]), publicUrl]))
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
if (URL.canParse(publicUrl) === false) {
|
|
32
|
-
useLogger().error('Invalid PUBLIC_URL for login redirect');
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
const { protocol: redirectProtocol, host: redirectHost } = new URL(redirect);
|
|
36
|
-
const { protocol: publicProtocol, host: publicHost } = new URL(publicUrl);
|
|
37
|
-
// allow redirects to the defined PUBLIC_URL (protocol + host including port)
|
|
38
|
-
return `${redirectProtocol}//${redirectHost}` === `${publicProtocol}//${publicHost}`;
|
|
39
|
-
}
|