@directus/api 32.1.0 → 32.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/chat/constants/system-prompt.d.ts +1 -0
- package/dist/ai/chat/constants/system-prompt.js +51 -0
- package/dist/ai/chat/controllers/chat.post.d.ts +2 -0
- package/dist/ai/chat/controllers/chat.post.js +47 -0
- package/dist/ai/chat/lib/create-ui-stream.d.ts +15 -0
- package/dist/ai/chat/lib/create-ui-stream.js +42 -0
- package/dist/ai/chat/middleware/load-settings.d.ts +2 -0
- package/dist/ai/chat/middleware/load-settings.js +18 -0
- package/dist/ai/chat/models/chat-request.d.ts +34 -0
- package/dist/ai/chat/models/chat-request.js +26 -0
- package/dist/ai/chat/models/providers.d.ts +9 -0
- package/dist/ai/chat/models/providers.js +9 -0
- package/dist/ai/chat/router.d.ts +1 -0
- package/dist/ai/chat/router.js +5 -0
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.d.ts +9 -0
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +38 -0
- package/dist/ai/chat/utils/fix-error-tool-calls.d.ts +12 -0
- package/dist/ai/chat/utils/fix-error-tool-calls.js +30 -0
- package/dist/ai/chat/utils/parse-json-schema-7.d.ts +13 -0
- package/dist/ai/chat/utils/parse-json-schema-7.js +75 -0
- package/dist/{mcp → ai/mcp}/server.d.ts +13 -16
- package/dist/{mcp → ai/mcp}/server.js +4 -13
- package/dist/ai/mcp/types.d.ts +15 -0
- package/dist/{mcp/tools/assets.js → ai/tools/assets/index.js} +8 -5
- package/dist/{mcp/tools/collections.js → ai/tools/collections/index.js} +7 -4
- package/dist/{mcp/tools/fields.js → ai/tools/fields/index.js} +12 -9
- package/dist/{mcp/tools/files.js → ai/tools/files/index.js} +11 -5
- package/dist/{mcp/tools/flows.js → ai/tools/flows/index.js} +11 -5
- package/dist/{mcp/tools/folders.js → ai/tools/folders/index.js} +12 -5
- package/dist/ai/tools/index.d.ts +15 -0
- package/dist/ai/tools/index.js +29 -0
- package/dist/{mcp/tools/items.js → ai/tools/items/index.js} +13 -6
- package/dist/{mcp/tools/prompts/items.md → ai/tools/items/prompt.md} +19 -15
- package/dist/{mcp/tools/operations.d.ts → ai/tools/operations/index.d.ts} +46 -0
- package/dist/{mcp/tools/operations.js → ai/tools/operations/index.js} +12 -5
- package/dist/{mcp/tools/relations.js → ai/tools/relations/index.js} +7 -4
- package/dist/{mcp/tools/schema.d.ts → ai/tools/schema/index.d.ts} +1 -1
- package/dist/{mcp/tools/schema.js → ai/tools/schema/index.js} +9 -6
- package/dist/{mcp/tools/system.js → ai/tools/system/index.js} +7 -4
- package/dist/{mcp/tools/trigger-flow.js → ai/tools/trigger-flow/index.js} +8 -5
- package/dist/{mcp → ai/tools}/types.d.ts +1 -17
- package/dist/ai/tools/utils.d.ts +9 -0
- package/dist/ai/tools/utils.js +17 -0
- package/dist/app.js +5 -0
- package/dist/auth/drivers/oauth2.d.ts +2 -1
- package/dist/auth/drivers/oauth2.js +17 -22
- package/dist/auth/drivers/openid.d.ts +2 -1
- package/dist/auth/drivers/openid.js +13 -18
- package/dist/auth/drivers/saml.js +6 -3
- package/dist/controllers/assets.js +39 -2
- package/dist/controllers/mcp.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
- package/dist/database/migrations/20251103A-add-ai-settings.d.ts +3 -0
- package/dist/database/migrations/20251103A-add-ai-settings.js +14 -0
- package/dist/database/run-ast/run-ast.js +1 -1
- package/dist/extensions/lib/installation/manager.js +5 -9
- package/dist/extensions/lib/sync/status.d.ts +11 -0
- package/dist/extensions/lib/sync/status.js +34 -0
- package/dist/extensions/lib/sync/sync.d.ts +6 -0
- package/dist/extensions/lib/sync/sync.js +90 -0
- package/dist/extensions/lib/sync/tracker.d.ts +18 -0
- package/dist/extensions/lib/sync/tracker.js +71 -0
- package/dist/extensions/lib/sync/utils.d.ts +24 -0
- package/dist/extensions/lib/sync/utils.js +62 -0
- package/dist/extensions/manager.d.ts +8 -4
- package/dist/extensions/manager.js +30 -13
- package/dist/middleware/respond.js +2 -2
- package/dist/permissions/lib/fetch-policies.d.ts +1 -1
- package/dist/permissions/lib/fetch-roles-tree.d.ts +6 -3
- package/dist/permissions/lib/fetch-roles-tree.js +5 -27
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +9 -7
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +17 -9
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +1 -1
- package/dist/permissions/utils/fetch-raw-permissions.d.ts +1 -1
- package/dist/permissions/utils/fetch-share-info.d.ts +1 -1
- package/dist/permissions/utils/fetch-share-info.js +1 -1
- package/dist/permissions/utils/filter-policies-by-ip.js +1 -1
- package/dist/permissions/utils/get-permissions-for-share.js +8 -8
- package/dist/permissions/utils/with-cache.d.ts +8 -6
- package/dist/permissions/utils/with-cache.js +12 -10
- package/dist/request/is-denied-ip.js +2 -2
- package/dist/services/assets/name-deduper.d.ts +7 -0
- package/dist/services/assets/name-deduper.js +23 -0
- package/dist/services/assets.d.ts +15 -2
- package/dist/services/assets.js +98 -5
- package/dist/services/authentication.js +4 -4
- package/dist/services/comments.js +2 -2
- package/dist/services/extensions.js +4 -0
- package/dist/services/folders.d.ts +27 -2
- package/dist/services/folders.js +75 -0
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/import-export.d.ts +1 -1
- package/dist/services/import-export.js +4 -5
- package/dist/services/notifications.js +2 -2
- package/dist/services/payload.js +20 -0
- package/dist/services/roles.js +2 -2
- package/dist/services/tus/server.js +3 -3
- package/dist/telemetry/utils/get-settings.d.ts +15 -0
- package/dist/telemetry/utils/get-settings.js +25 -9
- package/dist/test-utils/README.md +95 -24
- package/dist/test-utils/cache.d.ts +2 -2
- package/dist/test-utils/cache.js +2 -2
- package/dist/test-utils/{fields-service.d.ts → services/fields-service.d.ts} +1 -1
- package/dist/test-utils/{fields-service.js → services/fields-service.js} +3 -2
- package/dist/test-utils/services/files-service.d.ts +28 -0
- package/dist/test-utils/services/files-service.js +34 -0
- package/dist/test-utils/services/folders-service.d.ts +28 -0
- package/dist/test-utils/services/folders-service.js +33 -0
- package/dist/utils/encrypt.d.ts +2 -0
- package/dist/utils/encrypt.js +64 -0
- package/dist/utils/get-accountability-for-role.js +2 -2
- package/dist/utils/get-accountability-for-token.js +4 -4
- package/dist/utils/get-cache-key.js +2 -2
- package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
- package/dist/{auth/utils → utils}/is-login-redirect-allowed.js +8 -16
- package/dist/utils/require-text.d.ts +1 -0
- package/dist/utils/require-text.js +4 -0
- package/dist/utils/require-yaml.js +2 -2
- package/package.json +31 -25
- package/dist/auth/utils/generate-callback-url.d.ts +0 -8
- package/dist/auth/utils/generate-callback-url.js +0 -11
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -8
- package/dist/extensions/lib/sync-extensions.d.ts +0 -3
- package/dist/extensions/lib/sync-extensions.js +0 -70
- package/dist/extensions/lib/sync-status.d.ts +0 -10
- package/dist/extensions/lib/sync-status.js +0 -27
- package/dist/mcp/tools/index.d.ts +0 -15
- package/dist/mcp/tools/index.js +0 -29
- package/dist/mcp/tools/prompts/index.d.ts +0 -16
- package/dist/mcp/tools/prompts/index.js +0 -19
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
- package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
- package/dist/utils/get-date-formatted.d.ts +0 -1
- package/dist/utils/get-date-formatted.js +0 -10
- package/dist/utils/ip-in-networks.d.ts +0 -6
- package/dist/utils/ip-in-networks.js +0 -13
- /package/dist/{mcp → ai/mcp}/index.d.ts +0 -0
- /package/dist/{mcp → ai/mcp}/index.js +0 -0
- /package/dist/{mcp → ai/mcp}/transport.d.ts +0 -0
- /package/dist/{mcp → ai/mcp}/transport.js +0 -0
- /package/dist/{mcp → ai/mcp}/types.js +0 -0
- /package/dist/{mcp/tools/assets.d.ts → ai/tools/assets/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/assets.md → ai/tools/assets/prompt.md} +0 -0
- /package/dist/{mcp/tools/collections.d.ts → ai/tools/collections/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/collections.md → ai/tools/collections/prompt.md} +0 -0
- /package/dist/{mcp/define.d.ts → ai/tools/define-tool.d.ts} +0 -0
- /package/dist/{mcp/define.js → ai/tools/define-tool.js} +0 -0
- /package/dist/{mcp/tools/fields.d.ts → ai/tools/fields/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/fields.md → ai/tools/fields/prompt.md} +0 -0
- /package/dist/{mcp/tools/files.d.ts → ai/tools/files/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/files.md → ai/tools/files/prompt.md} +0 -0
- /package/dist/{mcp/tools/flows.d.ts → ai/tools/flows/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/flows.md → ai/tools/flows/prompt.md} +0 -0
- /package/dist/{mcp/tools/folders.d.ts → ai/tools/folders/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/folders.md → ai/tools/folders/prompt.md} +0 -0
- /package/dist/{mcp/tools/items.d.ts → ai/tools/items/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/operations.md → ai/tools/operations/prompt.md} +0 -0
- /package/dist/{mcp/tools/relations.d.ts → ai/tools/relations/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/relations.md → ai/tools/relations/prompt.md} +0 -0
- /package/dist/{mcp/tools/prompts/schema.md → ai/tools/schema/prompt.md} +0 -0
- /package/dist/{mcp → ai/tools}/schema.d.ts +0 -0
- /package/dist/{mcp → ai/tools}/schema.js +0 -0
- /package/dist/{mcp/tools/system.d.ts → ai/tools/system/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/system-prompt-description.md → ai/tools/system/prompt-description.md} +0 -0
- /package/dist/{mcp/tools/prompts/system-prompt.md → ai/tools/system/prompt.md} +0 -0
- /package/dist/{mcp/tools/trigger-flow.d.ts → ai/tools/trigger-flow/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/trigger-flow.md → ai/tools/trigger-flow/prompt.md} +0 -0
- /package/dist/{permissions/modules/fetch-global-access → ai/tools}/types.js +0 -0
- /package/dist/test-utils/{items-service.d.ts → services/items-service.d.ts} +0 -0
- /package/dist/test-utils/{items-service.js → services/items-service.js} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* ensuring key order.
|
|
2
|
+
* Wraps a function with caching capabilities.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* @param namespace - A unique namespace for the cache key.
|
|
5
|
+
* @param handler - The function to be wrapped.
|
|
6
|
+
* @param prepareArg - Optional function to prepare arguments for hashing.
|
|
7
|
+
* @returns A new function that caches the results of the original function.
|
|
7
8
|
*
|
|
8
|
-
* @NOTE
|
|
9
|
+
* @NOTE Ensure that the `namespace` is unique to avoid cache key collisions.
|
|
10
|
+
* @NOTE Ensure that the `prepareArg` function returns a JSON stringifiable representation of the arguments.
|
|
9
11
|
*/
|
|
10
|
-
export declare function withCache<F extends (
|
|
12
|
+
export declare function withCache<F extends (...args: any) => any>(namespace: string, handler: F, prepareArg?: (...args: Parameters<F>) => Record<string, unknown>): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { getSimpleHash } from '@directus/utils';
|
|
2
2
|
import { useCache } from '../cache.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* ensuring key order.
|
|
4
|
+
* Wraps a function with caching capabilities.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* @param namespace - A unique namespace for the cache key.
|
|
7
|
+
* @param handler - The function to be wrapped.
|
|
8
|
+
* @param prepareArg - Optional function to prepare arguments for hashing.
|
|
9
|
+
* @returns A new function that caches the results of the original function.
|
|
9
10
|
*
|
|
10
|
-
* @NOTE
|
|
11
|
+
* @NOTE Ensure that the `namespace` is unique to avoid cache key collisions.
|
|
12
|
+
* @NOTE Ensure that the `prepareArg` function returns a JSON stringifiable representation of the arguments.
|
|
11
13
|
*/
|
|
12
14
|
export function withCache(namespace, handler, prepareArg) {
|
|
13
15
|
const cache = useCache();
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
const key = namespace + '-' + getSimpleHash(JSON.stringify(
|
|
16
|
+
return async (...args) => {
|
|
17
|
+
const hashArgs = prepareArg ? prepareArg(...args) : args;
|
|
18
|
+
const key = namespace + '-' + getSimpleHash(JSON.stringify(hashArgs));
|
|
17
19
|
const cached = await cache.get(key);
|
|
18
20
|
if (cached !== undefined) {
|
|
19
21
|
return cached;
|
|
20
22
|
}
|
|
21
|
-
const res = await handler(
|
|
23
|
+
const res = await handler(...args);
|
|
22
24
|
cache.set(key, res);
|
|
23
25
|
return res;
|
|
24
|
-
}
|
|
26
|
+
};
|
|
25
27
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import
|
|
2
|
+
import { ipInNetworks } from '@directus/utils/node';
|
|
3
3
|
import { matches } from 'ip-matching';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import { useLogger } from '../logger/index.js';
|
|
5
|
-
import { ipInNetworks } from '../utils/ip-in-networks.js';
|
|
6
6
|
export function isDeniedIp(ip) {
|
|
7
7
|
const env = useEnv();
|
|
8
8
|
const logger = useLogger();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sanitize from 'sanitize-filename';
|
|
2
|
+
const DEFAULT_GROUP = Symbol('undefined');
|
|
3
|
+
export class NameDeduper {
|
|
4
|
+
map = {};
|
|
5
|
+
add(name, options) {
|
|
6
|
+
name = sanitize(name ?? '') || options?.fallback;
|
|
7
|
+
if (!name) {
|
|
8
|
+
throw Error('Invalid "name" provided');
|
|
9
|
+
}
|
|
10
|
+
const groupKey = options?.group ?? DEFAULT_GROUP;
|
|
11
|
+
const match = this.map[groupKey]?.[name];
|
|
12
|
+
if (match) {
|
|
13
|
+
const dedupedName = `${name} (${match})`;
|
|
14
|
+
this.map[groupKey][name] += 1;
|
|
15
|
+
return dedupedName;
|
|
16
|
+
}
|
|
17
|
+
if (!this.map[groupKey]) {
|
|
18
|
+
this.map[groupKey] = {};
|
|
19
|
+
}
|
|
20
|
+
this.map[groupKey][name] = 1;
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, Accountability, Range,
|
|
1
|
+
import type { AbstractServiceOptions, Accountability, Range, SchemaOverview, Stat, TransformationSet } from '@directus/types';
|
|
2
|
+
import archiver from 'archiver';
|
|
2
3
|
import type { Knex } from 'knex';
|
|
3
4
|
import type { Readable } from 'node:stream';
|
|
4
5
|
import { FilesService } from './files.js';
|
|
@@ -6,8 +7,20 @@ export declare class AssetsService {
|
|
|
6
7
|
knex: Knex;
|
|
7
8
|
accountability: Accountability | null;
|
|
8
9
|
schema: SchemaOverview;
|
|
9
|
-
|
|
10
|
+
sudoFilesService: FilesService;
|
|
10
11
|
constructor(options: AbstractServiceOptions);
|
|
12
|
+
private zip;
|
|
13
|
+
zipFiles(files: string[]): Promise<{
|
|
14
|
+
archive: archiver.Archiver;
|
|
15
|
+
complete: () => Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
zipFolder(root: string): Promise<{
|
|
18
|
+
archive: archiver.Archiver;
|
|
19
|
+
complete: () => Promise<void>;
|
|
20
|
+
metadata: {
|
|
21
|
+
name: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
11
24
|
getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: false): Promise<{
|
|
12
25
|
stream: Readable;
|
|
13
26
|
file: any;
|
package/dist/services/assets.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { ForbiddenError, IllegalAssetTransformationError, InvalidQueryError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
|
|
2
|
+
import { ForbiddenError, IllegalAssetTransformationError, InvalidPayloadError, InvalidQueryError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
|
|
3
|
+
import archiver from 'archiver';
|
|
3
4
|
import { clamp } from 'lodash-es';
|
|
4
|
-
import { contentType } from 'mime-types';
|
|
5
|
+
import { contentType, extension } from 'mime-types';
|
|
5
6
|
import hash from 'object-hash';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import sharp from 'sharp';
|
|
@@ -13,20 +14,112 @@ import { getStorage } from '../storage/index.js';
|
|
|
13
14
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
14
15
|
import { isValidUuid } from '../utils/is-valid-uuid.js';
|
|
15
16
|
import * as TransformationUtils from '../utils/transformations.js';
|
|
17
|
+
import { NameDeduper } from './assets/name-deduper.js';
|
|
16
18
|
import { FilesService } from './files.js';
|
|
17
19
|
import { getSharpInstance } from './files/lib/get-sharp-instance.js';
|
|
20
|
+
import { FoldersService } from './folders.js';
|
|
18
21
|
const env = useEnv();
|
|
19
22
|
const logger = useLogger();
|
|
20
23
|
export class AssetsService {
|
|
21
24
|
knex;
|
|
22
25
|
accountability;
|
|
23
26
|
schema;
|
|
24
|
-
|
|
27
|
+
sudoFilesService;
|
|
25
28
|
constructor(options) {
|
|
26
29
|
this.knex = options.knex || getDatabase();
|
|
27
30
|
this.accountability = options.accountability || null;
|
|
28
31
|
this.schema = options.schema;
|
|
29
|
-
this.
|
|
32
|
+
this.sudoFilesService = new FilesService({ ...options, accountability: null });
|
|
33
|
+
}
|
|
34
|
+
zip(options) {
|
|
35
|
+
if (options.files.length === 0) {
|
|
36
|
+
throw new InvalidPayloadError({ reason: 'No files found in the selected folders tree' });
|
|
37
|
+
}
|
|
38
|
+
const archive = archiver('zip');
|
|
39
|
+
const complete = async () => {
|
|
40
|
+
const deduper = new NameDeduper();
|
|
41
|
+
const storage = await getStorage();
|
|
42
|
+
for (const { id, folder, filename_download } of options.files) {
|
|
43
|
+
const file = await this.sudoFilesService.readOne(id, {
|
|
44
|
+
fields: ['id', 'storage', 'filename_disk', 'filename_download', 'modified_on', 'type'],
|
|
45
|
+
});
|
|
46
|
+
const exists = await storage.location(file.storage).exists(file.filename_disk);
|
|
47
|
+
if (!exists)
|
|
48
|
+
throw new ForbiddenError();
|
|
49
|
+
const version = file.modified_on ? (new Date(file.modified_on).getTime() / 1000).toFixed() : undefined;
|
|
50
|
+
const assetStream = await storage.location(file.storage).read(file.filename_disk, { version });
|
|
51
|
+
const fileExtension = path.extname(file.filename_download) || (file.type && '.' + extension(file.type)) || '';
|
|
52
|
+
const dedupedFileName = deduper.add(filename_download, { group: folder, fallback: file.id + fileExtension });
|
|
53
|
+
const folderName = folder ? options.folders?.get(folder) : undefined;
|
|
54
|
+
archive.append(assetStream, { name: dedupedFileName, prefix: folderName });
|
|
55
|
+
}
|
|
56
|
+
// add any empty folders, does not override already filled folder
|
|
57
|
+
if (options.folders) {
|
|
58
|
+
for (const [, folder] of options.folders) {
|
|
59
|
+
archive.append('', { name: folder + '/' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await archive.finalize();
|
|
63
|
+
};
|
|
64
|
+
return { archive, complete };
|
|
65
|
+
}
|
|
66
|
+
async zipFiles(files) {
|
|
67
|
+
const filesService = new FilesService({
|
|
68
|
+
schema: this.schema,
|
|
69
|
+
knex: this.knex,
|
|
70
|
+
accountability: this.accountability,
|
|
71
|
+
});
|
|
72
|
+
const filesToZip = await filesService.readByQuery({
|
|
73
|
+
filter: {
|
|
74
|
+
id: {
|
|
75
|
+
_in: files,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
limit: -1,
|
|
79
|
+
});
|
|
80
|
+
return this.zip({
|
|
81
|
+
files: filesToZip.map((file) => ({
|
|
82
|
+
id: file['id'],
|
|
83
|
+
folder: file['folder'],
|
|
84
|
+
filename_download: file['filename_download'],
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async zipFolder(root) {
|
|
89
|
+
const foldersService = new FoldersService({
|
|
90
|
+
schema: this.schema,
|
|
91
|
+
knex: this.knex,
|
|
92
|
+
accountability: this.accountability,
|
|
93
|
+
});
|
|
94
|
+
const folderTree = await foldersService.buildTree(root);
|
|
95
|
+
const filesService = new FilesService({
|
|
96
|
+
schema: this.schema,
|
|
97
|
+
knex: this.knex,
|
|
98
|
+
accountability: this.accountability,
|
|
99
|
+
});
|
|
100
|
+
const filesToZip = await filesService.readByQuery({
|
|
101
|
+
filter: {
|
|
102
|
+
folder: {
|
|
103
|
+
_in: Array.from(folderTree.keys()),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
limit: -1,
|
|
107
|
+
});
|
|
108
|
+
const { archive, complete } = this.zip({
|
|
109
|
+
folders: folderTree,
|
|
110
|
+
files: filesToZip.map((file) => ({
|
|
111
|
+
id: file['id'],
|
|
112
|
+
folder: file['folder'],
|
|
113
|
+
filename_download: file['filename_download'],
|
|
114
|
+
})),
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
archive,
|
|
118
|
+
complete,
|
|
119
|
+
metadata: {
|
|
120
|
+
name: folderTree.get(root),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
30
123
|
}
|
|
31
124
|
async getAsset(id, transformation, range, deferStream = false) {
|
|
32
125
|
const storage = await getStorage();
|
|
@@ -50,7 +143,7 @@ export class AssetsService {
|
|
|
50
143
|
primaryKeys: [id],
|
|
51
144
|
}, { knex: this.knex, schema: this.schema });
|
|
52
145
|
}
|
|
53
|
-
const file = (await this.
|
|
146
|
+
const file = (await this.sudoFilesService.readOne(id, { limit: 1 }));
|
|
54
147
|
const exists = await storage.location(file.storage).exists(file.filename_disk);
|
|
55
148
|
if (!exists)
|
|
56
149
|
throw new ForbiddenError();
|
|
@@ -149,8 +149,8 @@ export class AuthenticationService {
|
|
|
149
149
|
throw new InvalidOtpError();
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
const roles = await fetchRolesTree(user.role, this.knex);
|
|
153
|
-
const globalAccess = await fetchGlobalAccess({ roles, user: user.id, ip: this.accountability?.ip ?? null }, this.knex);
|
|
152
|
+
const roles = await fetchRolesTree(user.role, { knex: this.knex });
|
|
153
|
+
const globalAccess = await fetchGlobalAccess({ roles, user: user.id, ip: this.accountability?.ip ?? null }, { knex: this.knex });
|
|
154
154
|
const tokenPayload = {
|
|
155
155
|
id: user.id,
|
|
156
156
|
role: user.role,
|
|
@@ -277,8 +277,8 @@ export class AuthenticationService {
|
|
|
277
277
|
throw new InvalidCredentialsError();
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
-
const roles = await fetchRolesTree(record.user_role, this.knex);
|
|
281
|
-
const globalAccess = await fetchGlobalAccess({ user: record.user_id, roles, ip: this.accountability?.ip ?? null }, this.knex);
|
|
280
|
+
const roles = await fetchRolesTree(record.user_role, { knex: this.knex });
|
|
281
|
+
const globalAccess = await fetchGlobalAccess({ user: record.user_id, roles, ip: this.accountability?.ip ?? null }, { knex: this.knex });
|
|
282
282
|
if (record.user_id) {
|
|
283
283
|
const provider = getAuthProvider(record.user_provider);
|
|
284
284
|
await provider.refresh({
|
|
@@ -63,10 +63,10 @@ export class CommentsService extends ItemsService {
|
|
|
63
63
|
role: user['role']?.id ?? null,
|
|
64
64
|
admin: false,
|
|
65
65
|
app: false,
|
|
66
|
-
roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
|
|
66
|
+
roles: await fetchRolesTree(user['role']?.id ?? null, { knex: this.knex }),
|
|
67
67
|
ip: null,
|
|
68
68
|
};
|
|
69
|
-
const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
|
|
69
|
+
const userGlobalAccess = await fetchGlobalAccess(accountability, { knex: this.knex });
|
|
70
70
|
accountability.admin = userGlobalAccess.admin;
|
|
71
71
|
accountability.app = userGlobalAccess.app;
|
|
72
72
|
const usersService = new UsersService({ schema: this.schema, accountability });
|
|
@@ -109,6 +109,8 @@ export class ExtensionsService {
|
|
|
109
109
|
await this.extensionsManager.install(versionId);
|
|
110
110
|
}
|
|
111
111
|
async readAll() {
|
|
112
|
+
// wait for extensions to be reloaded
|
|
113
|
+
await this.extensionsManager.isReloading();
|
|
112
114
|
const settings = await this.extensionsItemService.readByQuery({ limit: -1 });
|
|
113
115
|
const regular = settings.filter(({ bundle }) => bundle === null);
|
|
114
116
|
const bundled = settings.filter(({ bundle }) => bundle !== null);
|
|
@@ -138,6 +140,8 @@ export class ExtensionsService {
|
|
|
138
140
|
return output;
|
|
139
141
|
}
|
|
140
142
|
async readOne(id) {
|
|
143
|
+
// wait for extensions to be reloaded
|
|
144
|
+
await this.extensionsManager.isReloading();
|
|
141
145
|
const meta = await this.extensionsItemService.readOne(id);
|
|
142
146
|
const schema = this.extensionsManager.getExtension(meta.source, meta.folder) ?? null;
|
|
143
147
|
return {
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
-
import type { AbstractServiceOptions } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, Folder } from '@directus/types';
|
|
2
2
|
import { ItemsService } from './items.js';
|
|
3
|
-
export declare class FoldersService extends ItemsService {
|
|
3
|
+
export declare class FoldersService extends ItemsService<Folder> {
|
|
4
4
|
constructor(options: AbstractServiceOptions);
|
|
5
|
+
/**
|
|
6
|
+
* Builds a full folder tree starting from a given root folder.
|
|
7
|
+
*
|
|
8
|
+
* This method returns a map of folder IDs to their corresponding paths
|
|
9
|
+
* relative to the root. It resolves all nested child folders and ensures
|
|
10
|
+
* that folder names are deduplicated within the same parent.
|
|
11
|
+
*
|
|
12
|
+
* Access control is applied automatically when non-admin, only folders the user has `read`
|
|
13
|
+
* access to are included.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} root - The ID of the root folder to start building the tree from.
|
|
16
|
+
* @returns {Promise<Map<string, string>>} A `Map` where:
|
|
17
|
+
* - Key: folder ID
|
|
18
|
+
* - Value: folder path relative to the root (e.g., "Documents/Photos")
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const foldersService = new FoldersService({ schema, accountability });
|
|
22
|
+
* const tree = await foldersService.buildTree('root-folder-id');
|
|
23
|
+
* console.log(tree.get('folder1')); // e.g., "RootFolder/SubFolder1"
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* - The returned `Map` includes the root folder itself.
|
|
27
|
+
* - If a folder has no name, its ID will be used as a fallback.
|
|
28
|
+
*/
|
|
29
|
+
buildTree(root: string): Promise<Map<string, string>>;
|
|
5
30
|
}
|
package/dist/services/folders.js
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
|
+
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
2
|
+
import { NameDeduper } from './assets/name-deduper.js';
|
|
1
3
|
import { ItemsService } from './items.js';
|
|
2
4
|
export class FoldersService extends ItemsService {
|
|
3
5
|
constructor(options) {
|
|
4
6
|
super('directus_folders', options);
|
|
5
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds a full folder tree starting from a given root folder.
|
|
10
|
+
*
|
|
11
|
+
* This method returns a map of folder IDs to their corresponding paths
|
|
12
|
+
* relative to the root. It resolves all nested child folders and ensures
|
|
13
|
+
* that folder names are deduplicated within the same parent.
|
|
14
|
+
*
|
|
15
|
+
* Access control is applied automatically when non-admin, only folders the user has `read`
|
|
16
|
+
* access to are included.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} root - The ID of the root folder to start building the tree from.
|
|
19
|
+
* @returns {Promise<Map<string, string>>} A `Map` where:
|
|
20
|
+
* - Key: folder ID
|
|
21
|
+
* - Value: folder path relative to the root (e.g., "Documents/Photos")
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const foldersService = new FoldersService({ schema, accountability });
|
|
25
|
+
* const tree = await foldersService.buildTree('root-folder-id');
|
|
26
|
+
* console.log(tree.get('folder1')); // e.g., "RootFolder/SubFolder1"
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* - The returned `Map` includes the root folder itself.
|
|
30
|
+
* - If a folder has no name, its ID will be used as a fallback.
|
|
31
|
+
*/
|
|
32
|
+
async buildTree(root) {
|
|
33
|
+
if (this.accountability && this.accountability.admin !== true) {
|
|
34
|
+
await validateAccess({
|
|
35
|
+
collection: 'directus_folders',
|
|
36
|
+
accountability: this.accountability,
|
|
37
|
+
action: 'read',
|
|
38
|
+
primaryKeys: [root],
|
|
39
|
+
}, {
|
|
40
|
+
knex: this.knex,
|
|
41
|
+
schema: this.schema,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const folders = await this.readByQuery({ limit: -1 });
|
|
45
|
+
// build folder and child lookup
|
|
46
|
+
const folderLookup = new Map();
|
|
47
|
+
const childFolderLookup = new Map();
|
|
48
|
+
for (const folder of folders) {
|
|
49
|
+
if (!folder['id'])
|
|
50
|
+
continue;
|
|
51
|
+
folderLookup.set(folder['id'], folder);
|
|
52
|
+
// root is always at the top level, we can therfor safely skip any parent references to it.
|
|
53
|
+
if (folder['parent'] && folder['id'] !== root) {
|
|
54
|
+
const children = childFolderLookup.get(folder['parent']) ?? [];
|
|
55
|
+
children.push(folder['id']);
|
|
56
|
+
childFolderLookup.set(folder['parent'], children);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const deduper = new NameDeduper();
|
|
60
|
+
const rootName = deduper.add(folderLookup.get(root)?.name, { fallback: root });
|
|
61
|
+
const stack = [[root, '']];
|
|
62
|
+
const tree = new Map();
|
|
63
|
+
// build tree from stack
|
|
64
|
+
while (stack.length > 0) {
|
|
65
|
+
const [folderId, path] = stack.pop() ?? [];
|
|
66
|
+
if (!folderId)
|
|
67
|
+
continue;
|
|
68
|
+
const folder = folderLookup.get(folderId);
|
|
69
|
+
if (!folder)
|
|
70
|
+
continue;
|
|
71
|
+
const children = childFolderLookup.get(folderId);
|
|
72
|
+
const folderName = deduper.add(folder['name'], { group: folder['parent'], fallback: folderId });
|
|
73
|
+
const folderPath = path === '' ? rootName : `${path}/${folderName}`;
|
|
74
|
+
tree.set(folderId, folderPath);
|
|
75
|
+
for (const childFolderId of children ?? []) {
|
|
76
|
+
stack.push([childFolderId, folderPath]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return tree;
|
|
80
|
+
}
|
|
6
81
|
}
|
|
@@ -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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AbstractServiceOptions, Accountability, DirectusError, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { Readable } from 'node:stream';
|
|
4
|
-
import type {
|
|
4
|
+
import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../types/index.js';
|
|
5
5
|
export declare function createErrorTracker(): {
|
|
6
6
|
addCapturedError: (err: any, rowNumber: number) => void;
|
|
7
7
|
buildFinalErrors: () => DirectusError<any>[];
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { createError, ErrorCode, ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
|
|
3
3
|
import { isSystemCollection } from '@directus/system-data';
|
|
4
|
-
import { parseJSON, toArray } from '@directus/utils';
|
|
4
|
+
import { getDateTimeFormatted, parseJSON, toArray } from '@directus/utils';
|
|
5
5
|
import { createTmpFile } from '@directus/utils/node';
|
|
6
6
|
import { queue } from 'async';
|
|
7
7
|
import destroyStream from 'destroy';
|
|
8
8
|
import { dump as toYAML } from 'js-yaml';
|
|
9
9
|
import { parse as toXML } from 'js2xmlparser';
|
|
10
10
|
import { Parser as CSVParser, transforms as CSVTransforms } from 'json2csv';
|
|
11
|
+
import { set } from 'lodash-es';
|
|
11
12
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
13
|
import { appendFile } from 'node:fs/promises';
|
|
13
14
|
import Papa from 'papaparse';
|
|
14
15
|
import StreamArray from 'stream-json/streamers/StreamArray.js';
|
|
16
|
+
import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
|
|
15
17
|
import getDatabase from '../database/index.js';
|
|
16
18
|
import emitter from '../emitter.js';
|
|
17
19
|
import { useLogger } from '../logger/index.js';
|
|
18
20
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
19
|
-
import { getDateFormatted } from '../utils/get-date-formatted.js';
|
|
20
21
|
import { getService } from '../utils/get-service.js';
|
|
21
22
|
import { transaction } from '../utils/transaction.js';
|
|
22
23
|
import { Url } from '../utils/url.js';
|
|
@@ -24,8 +25,6 @@ import { userName } from '../utils/user-name.js';
|
|
|
24
25
|
import { FilesService } from './files.js';
|
|
25
26
|
import { NotificationsService } from './notifications.js';
|
|
26
27
|
import { UsersService } from './users.js';
|
|
27
|
-
import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
|
|
28
|
-
import { set } from 'lodash-es';
|
|
29
28
|
const env = useEnv();
|
|
30
29
|
const logger = useLogger();
|
|
31
30
|
const MAX_IMPORT_ERRORS = env['MAX_IMPORT_ERRORS'];
|
|
@@ -514,7 +513,7 @@ export class ExportService {
|
|
|
514
513
|
schema: this.schema,
|
|
515
514
|
});
|
|
516
515
|
const storage = toArray(env['STORAGE_LOCATIONS'])[0];
|
|
517
|
-
const title = `export-${collection}-${
|
|
516
|
+
const title = `export-${collection}-${getDateTimeFormatted()}`;
|
|
518
517
|
const filename = `${title}.${format}`;
|
|
519
518
|
const fileWithDefaults = {
|
|
520
519
|
...(options?.file ?? {}),
|
|
@@ -29,12 +29,12 @@ export class NotificationsService extends ItemsService {
|
|
|
29
29
|
.addPath('admin', 'users', user['id'])
|
|
30
30
|
.toString();
|
|
31
31
|
const html = data.message ? md(data.message) : '';
|
|
32
|
-
const roles = await fetchRolesTree(user['role'], this.knex);
|
|
32
|
+
const roles = await fetchRolesTree(user['role'], { knex: this.knex });
|
|
33
33
|
const { app: app_access } = await fetchGlobalAccess({
|
|
34
34
|
user: user['id'],
|
|
35
35
|
roles,
|
|
36
36
|
ip: null,
|
|
37
|
-
}, this.knex);
|
|
37
|
+
}, { knex: this.knex });
|
|
38
38
|
const mailService = new MailService({
|
|
39
39
|
schema: this.schema,
|
|
40
40
|
knex: this.knex,
|
package/dist/services/payload.js
CHANGED
|
@@ -10,6 +10,8 @@ import { parse as wktToGeoJSON } from 'wellknown';
|
|
|
10
10
|
import { getHelpers } from '../database/helpers/index.js';
|
|
11
11
|
import getDatabase from '../database/index.js';
|
|
12
12
|
import { generateHash } from '../utils/generate-hash.js';
|
|
13
|
+
import { decrypt, encrypt } from '../utils/encrypt.js';
|
|
14
|
+
import { getSecret } from '../utils/get-secret.js';
|
|
13
15
|
/**
|
|
14
16
|
* Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
|
|
15
17
|
* handled correctly.
|
|
@@ -124,6 +126,24 @@ export class PayloadService {
|
|
|
124
126
|
}
|
|
125
127
|
return value;
|
|
126
128
|
},
|
|
129
|
+
async encrypt({ action, value, accountability }) {
|
|
130
|
+
if (!value)
|
|
131
|
+
return value;
|
|
132
|
+
if (action === 'read') {
|
|
133
|
+
// In-system calls can still get the decrypted value
|
|
134
|
+
if (accountability === null) {
|
|
135
|
+
const key = getSecret();
|
|
136
|
+
return await decrypt(value, key);
|
|
137
|
+
}
|
|
138
|
+
// Requests from the API entrypoints have accountability and shouldn't get the raw value
|
|
139
|
+
return '**********';
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
const key = getSecret();
|
|
143
|
+
return await encrypt(value, key);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
},
|
|
127
147
|
};
|
|
128
148
|
async processValues(action, payload, aliasMap = {}, aggregate = {}) {
|
|
129
149
|
const processedPayload = toArray(payload);
|
package/dist/services/roles.js
CHANGED
|
@@ -3,8 +3,8 @@ import { UserIntegrityCheckFlag } from '@directus/types';
|
|
|
3
3
|
import { clearSystemCache } from '../cache.js';
|
|
4
4
|
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
5
5
|
import { transaction } from '../utils/transaction.js';
|
|
6
|
-
import { ItemsService } from './items.js';
|
|
7
6
|
import { AccessService } from './access.js';
|
|
7
|
+
import { ItemsService } from './items.js';
|
|
8
8
|
import { PresetsService } from './presets.js';
|
|
9
9
|
import { UsersService } from './users.js';
|
|
10
10
|
export class RolesService extends ItemsService {
|
|
@@ -72,7 +72,7 @@ export class RolesService extends ItemsService {
|
|
|
72
72
|
if (ids.includes(parent)) {
|
|
73
73
|
throw new InvalidPayloadError({ reason: 'A role cannot be a parent of itself' });
|
|
74
74
|
}
|
|
75
|
-
const roles = await fetchRolesTree(parent, this.knex);
|
|
75
|
+
const roles = await fetchRolesTree(parent, { knex: this.knex });
|
|
76
76
|
if (ids.some((id) => roles.includes(id))) {
|
|
77
77
|
// The role tree up from the parent already includes this role, so it would create a circular reference
|
|
78
78
|
throw new InvalidPayloadError({ reason: 'A role cannot have a parent that is already a descendant of itself' });
|
|
@@ -41,7 +41,7 @@ export async function createTusServer(context) {
|
|
|
41
41
|
datastore: store,
|
|
42
42
|
locker: getTusLocker(),
|
|
43
43
|
...(RESUMABLE_UPLOADS.MAX_SIZE !== null && { maxSize: RESUMABLE_UPLOADS.MAX_SIZE }),
|
|
44
|
-
async onUploadFinish(
|
|
44
|
+
async onUploadFinish(_req, upload) {
|
|
45
45
|
const schema = await getSchema();
|
|
46
46
|
const service = new ItemsService('directus_files', {
|
|
47
47
|
schema,
|
|
@@ -92,9 +92,9 @@ export async function createTusServer(context) {
|
|
|
92
92
|
key: fileData.id,
|
|
93
93
|
collection: 'directus_files',
|
|
94
94
|
}, {
|
|
95
|
-
database: getDatabase(),
|
|
96
95
|
schema,
|
|
97
|
-
|
|
96
|
+
database: getDatabase(),
|
|
97
|
+
accountability: context.accountability ?? null,
|
|
98
98
|
});
|
|
99
99
|
return {
|
|
100
100
|
headers: {
|
|
@@ -5,5 +5,20 @@ export type TelemetrySettings = {
|
|
|
5
5
|
mcp_allow_deletes: boolean;
|
|
6
6
|
mcp_system_prompt_enabled: boolean;
|
|
7
7
|
visual_editor_urls: number;
|
|
8
|
+
ai_openai_api_key: boolean;
|
|
9
|
+
ai_anthropic_api_key: boolean;
|
|
10
|
+
ai_system_prompt: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type DatabaseSettings = {
|
|
13
|
+
project_id: string;
|
|
14
|
+
mcp_enabled?: boolean;
|
|
15
|
+
mcp_allow_deletes?: boolean;
|
|
16
|
+
mcp_system_prompt_enabled?: boolean;
|
|
17
|
+
visual_editor_urls?: {
|
|
18
|
+
url: string;
|
|
19
|
+
}[];
|
|
20
|
+
ai_openai_api_key?: string;
|
|
21
|
+
ai_anthropic_api_key?: string;
|
|
22
|
+
ai_system_prompt?: string;
|
|
8
23
|
};
|
|
9
24
|
export declare const getSettings: (db: Knex) => Promise<TelemetrySettings>;
|