@directus/api 24.0.1 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +10 -4
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +20 -7
- package/dist/controllers/assets.js +2 -2
- package/dist/controllers/metrics.d.ts +2 -0
- package/dist/controllers/metrics.js +33 -0
- package/dist/controllers/server.js +1 -1
- package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
- package/dist/database/helpers/number/dialects/mssql.js +3 -3
- package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
- package/dist/database/helpers/number/dialects/oracle.js +2 -2
- package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
- package/dist/database/helpers/number/dialects/sqlite.js +2 -2
- package/dist/database/helpers/number/types.d.ts +2 -2
- package/dist/database/helpers/number/types.js +2 -2
- package/dist/database/index.js +3 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/lib/create-metrics.d.ts +15 -0
- package/dist/metrics/lib/create-metrics.js +239 -0
- package/dist/metrics/lib/use-metrics.d.ts +17 -0
- package/dist/metrics/lib/use-metrics.js +15 -0
- package/dist/metrics/types/metric.d.ts +1 -0
- package/dist/metrics/types/metric.js +1 -0
- package/dist/middleware/respond.js +7 -1
- package/dist/operations/condition/index.js +7 -2
- package/dist/schedules/metrics.d.ts +7 -0
- package/dist/schedules/metrics.js +44 -0
- package/dist/services/assets.d.ts +6 -1
- package/dist/services/assets.js +8 -6
- package/dist/services/fields.js +1 -1
- package/dist/services/graphql/errors/format.d.ts +6 -0
- package/dist/services/graphql/errors/format.js +14 -0
- package/dist/services/graphql/index.d.ts +5 -53
- package/dist/services/graphql/index.js +5 -2720
- package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
- package/dist/services/graphql/resolvers/mutation.js +74 -0
- package/dist/services/graphql/resolvers/query.d.ts +8 -0
- package/dist/services/graphql/resolvers/query.js +87 -0
- package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
- package/dist/services/graphql/resolvers/system-admin.js +236 -0
- package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
- package/dist/services/graphql/resolvers/system-global.js +435 -0
- package/dist/services/graphql/resolvers/system.d.ts +11 -0
- package/dist/services/graphql/resolvers/system.js +554 -0
- package/dist/services/graphql/schema/get-types.d.ts +12 -0
- package/dist/services/graphql/schema/get-types.js +223 -0
- package/dist/services/graphql/schema/index.d.ts +32 -0
- package/dist/services/graphql/schema/index.js +190 -0
- package/dist/services/graphql/schema/parse-args.d.ts +9 -0
- package/dist/services/graphql/schema/parse-args.js +35 -0
- package/dist/services/graphql/schema/parse-query.d.ts +7 -0
- package/dist/services/graphql/schema/parse-query.js +98 -0
- package/dist/services/graphql/schema/read.d.ts +12 -0
- package/dist/services/graphql/schema/read.js +653 -0
- package/dist/services/graphql/schema/write.d.ts +9 -0
- package/dist/services/graphql/schema/write.js +142 -0
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/subscription.js +7 -6
- package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
- package/dist/services/graphql/utils/aggrgate-query.js +32 -0
- package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
- package/dist/services/graphql/utils/replace-fragments.js +21 -0
- package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
- package/dist/services/graphql/utils/replace-funcs.js +21 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
- package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
- package/dist/services/items.js +0 -2
- package/dist/services/meta.js +25 -84
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +23 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +58 -21
- package/dist/utils/freeze-schema.d.ts +3 -0
- package/dist/utils/freeze-schema.js +31 -0
- package/dist/utils/get-accountability-for-token.js +1 -0
- package/dist/utils/get-milliseconds.js +1 -1
- package/dist/utils/get-schema.js +10 -5
- package/dist/utils/permissions-cachable.d.ts +8 -0
- package/dist/utils/permissions-cachable.js +38 -0
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/messages.d.ts +6 -6
- package/package.json +22 -19
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { parse as parseBytesConfiguration } from 'bytes';
|
|
3
3
|
import { getCache, setCacheValue } from '../cache.js';
|
|
4
|
+
import getDatabase from '../database/index.js';
|
|
4
5
|
import { useLogger } from '../logger/index.js';
|
|
5
6
|
import { ExportService } from '../services/import-export.js';
|
|
6
7
|
import asyncHandler from '../utils/async-handler.js';
|
|
@@ -9,6 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
|
|
|
9
10
|
import { getDateFormatted } from '../utils/get-date-formatted.js';
|
|
10
11
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
11
12
|
import { stringByteSize } from '../utils/get-string-byte-size.js';
|
|
13
|
+
import { permissionsCachable } from '../utils/permissions-cachable.js';
|
|
12
14
|
export const respond = asyncHandler(async (req, res) => {
|
|
13
15
|
const env = useEnv();
|
|
14
16
|
const logger = useLogger();
|
|
@@ -26,7 +28,11 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
26
28
|
cache &&
|
|
27
29
|
!req.sanitizedQuery.export &&
|
|
28
30
|
res.locals['cache'] !== false &&
|
|
29
|
-
exceedsMaxSize === false
|
|
31
|
+
exceedsMaxSize === false &&
|
|
32
|
+
(await permissionsCachable(req.collection, {
|
|
33
|
+
knex: getDatabase(),
|
|
34
|
+
schema: req.schema,
|
|
35
|
+
}, req.accountability))) {
|
|
30
36
|
const key = await getCacheKey(req);
|
|
31
37
|
try {
|
|
32
38
|
await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { validatePayload } from '@directus/utils';
|
|
2
1
|
import { defineOperationApi } from '@directus/extensions';
|
|
2
|
+
import { validatePayload } from '@directus/utils';
|
|
3
|
+
import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
|
|
3
4
|
export default defineOperationApi({
|
|
4
5
|
id: 'condition',
|
|
5
6
|
handler: ({ filter }, { data }) => {
|
|
6
7
|
const errors = validatePayload(filter, data, { requireAll: true });
|
|
7
8
|
if (errors.length > 0) {
|
|
8
|
-
|
|
9
|
+
// sanitize and format errors
|
|
10
|
+
const validationErrors = errors
|
|
11
|
+
.map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
|
|
12
|
+
.flat();
|
|
13
|
+
throw validationErrors;
|
|
9
14
|
}
|
|
10
15
|
else {
|
|
11
16
|
return null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toBoolean } from '@directus/utils';
|
|
3
|
+
import { scheduleJob } from 'node-schedule';
|
|
4
|
+
import { useLogger } from '../logger/index.js';
|
|
5
|
+
import { useMetrics } from '../metrics/index.js';
|
|
6
|
+
import { validateCron } from '../utils/schedule.js';
|
|
7
|
+
const METRICS_LOCK_TIMEOUT = 10 * 60 * 1000; // 10 mins
|
|
8
|
+
let lockedAt = 0;
|
|
9
|
+
const logger = useLogger();
|
|
10
|
+
const metrics = useMetrics();
|
|
11
|
+
export async function handleMetricsJob() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (lockedAt !== 0 && lockedAt > now - METRICS_LOCK_TIMEOUT) {
|
|
14
|
+
// ensure only generating metrics once per node
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
lockedAt = Date.now();
|
|
18
|
+
try {
|
|
19
|
+
await metrics?.generate();
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.warn(`An error was thrown while attempting metric generation`);
|
|
23
|
+
logger.warn(err);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
lockedAt = 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Schedule the metric generation
|
|
31
|
+
*
|
|
32
|
+
* @returns Whether or not metrics has been initialized
|
|
33
|
+
*/
|
|
34
|
+
export default async function schedule() {
|
|
35
|
+
const env = useEnv();
|
|
36
|
+
if (!toBoolean(env['METRICS_ENABLED'])) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (!validateCron(String(env['METRICS_SCHEDULE']))) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
scheduleJob('metrics', String(env['METRICS_SCHEDULE']), handleMetricsJob);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -10,9 +10,14 @@ export declare class AssetsService {
|
|
|
10
10
|
schema: SchemaOverview;
|
|
11
11
|
filesService: FilesService;
|
|
12
12
|
constructor(options: AbstractServiceOptions);
|
|
13
|
-
getAsset(id: string, transformation?: TransformationSet, range?: Range): Promise<{
|
|
13
|
+
getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: false): Promise<{
|
|
14
14
|
stream: Readable;
|
|
15
15
|
file: any;
|
|
16
16
|
stat: Stat;
|
|
17
17
|
}>;
|
|
18
|
+
getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: true): Promise<{
|
|
19
|
+
stream: () => Promise<Readable>;
|
|
20
|
+
file: any;
|
|
21
|
+
stat: Stat;
|
|
22
|
+
}>;
|
|
18
23
|
}
|
package/dist/services/assets.js
CHANGED
|
@@ -28,7 +28,7 @@ export class AssetsService {
|
|
|
28
28
|
this.schema = options.schema;
|
|
29
29
|
this.filesService = new FilesService({ ...options, accountability: null });
|
|
30
30
|
}
|
|
31
|
-
async getAsset(id, transformation, range) {
|
|
31
|
+
async getAsset(id, transformation, range, deferStream = false) {
|
|
32
32
|
const storage = await getStorage();
|
|
33
33
|
const publicSettings = await this.knex
|
|
34
34
|
.select('project_logo', 'public_background', 'public_foreground', 'public_favicon')
|
|
@@ -99,8 +99,9 @@ export class AssetsService {
|
|
|
99
99
|
file.type = contentType(assetFilename) || null;
|
|
100
100
|
}
|
|
101
101
|
if (exists) {
|
|
102
|
+
const assetStream = () => storage.location(file.storage).read(assetFilename, { range });
|
|
102
103
|
return {
|
|
103
|
-
stream:
|
|
104
|
+
stream: deferStream ? assetStream : await assetStream(),
|
|
104
105
|
file,
|
|
105
106
|
stat: await storage.location(file.storage).stat(assetFilename),
|
|
106
107
|
};
|
|
@@ -123,7 +124,6 @@ export class AssetsService {
|
|
|
123
124
|
reason: 'Server too busy',
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
|
-
const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
127
127
|
const transformer = getSharpInstance();
|
|
128
128
|
transformer.timeout({
|
|
129
129
|
seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
|
|
@@ -131,6 +131,7 @@ export class AssetsService {
|
|
|
131
131
|
if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
|
|
132
132
|
transformer.rotate();
|
|
133
133
|
transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
|
|
134
|
+
const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
134
135
|
readStream.on('error', (e) => {
|
|
135
136
|
logger.error(e, `Couldn't transform file ${file.id}`);
|
|
136
137
|
readStream.unpipe(transformer);
|
|
@@ -152,16 +153,17 @@ export class AssetsService {
|
|
|
152
153
|
throw error;
|
|
153
154
|
}
|
|
154
155
|
}
|
|
156
|
+
const assetStream = () => storage.location(file.storage).read(assetFilename, { range, version });
|
|
155
157
|
return {
|
|
156
|
-
stream:
|
|
158
|
+
stream: deferStream ? assetStream : await assetStream(),
|
|
157
159
|
stat: await storage.location(file.storage).stat(assetFilename),
|
|
158
160
|
file,
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
else {
|
|
162
|
-
const
|
|
164
|
+
const assetStream = () => storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
163
165
|
const stat = await storage.location(file.storage).stat(file.filename_disk);
|
|
164
|
-
return { stream:
|
|
166
|
+
return { stream: deferStream ? assetStream : await assetStream(), file, stat };
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
}
|
package/dist/services/fields.js
CHANGED
|
@@ -58,7 +58,7 @@ export class FieldsService {
|
|
|
58
58
|
if (!columnInfo) {
|
|
59
59
|
columnInfo = await this.schemaInspector.columnInfo();
|
|
60
60
|
if (schemaCacheIsEnabled) {
|
|
61
|
-
setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
|
|
61
|
+
await setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
if (collection) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DirectusError } from '@directus/errors';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatError(error: DirectusError | DirectusError[]): GraphQLError;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {} from '@directus/errors';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
import { set } from 'lodash-es';
|
|
4
|
+
/**
|
|
5
|
+
* Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
|
|
6
|
+
*/
|
|
7
|
+
export function formatError(error) {
|
|
8
|
+
if (Array.isArray(error)) {
|
|
9
|
+
set(error[0], 'extensions.code', error[0].code);
|
|
10
|
+
return new GraphQLError(error[0].message, undefined, undefined, undefined, undefined, error[0]);
|
|
11
|
+
}
|
|
12
|
+
set(error, 'extensions.code', error.code);
|
|
13
|
+
return new GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
|
|
14
|
+
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
|
|
4
|
-
import { GraphQLError, GraphQLSchema } from 'graphql';
|
|
5
|
-
import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose';
|
|
1
|
+
import type { Accountability, Item, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { FormattedExecutionResult, GraphQLSchema } from 'graphql';
|
|
6
3
|
import type { Knex } from 'knex';
|
|
7
4
|
import type { AbstractServiceOptions, GraphQLParams } from '../../types/index.js';
|
|
5
|
+
export type GQLScope = 'items' | 'system';
|
|
8
6
|
export declare class GraphQLService {
|
|
9
7
|
accountability: Accountability | null;
|
|
10
8
|
knex: Knex;
|
|
11
9
|
schema: SchemaOverview;
|
|
12
|
-
scope:
|
|
10
|
+
scope: GQLScope;
|
|
13
11
|
constructor(options: AbstractServiceOptions & {
|
|
14
|
-
scope:
|
|
12
|
+
scope: GQLScope;
|
|
15
13
|
});
|
|
16
14
|
/**
|
|
17
15
|
* Execute a GraphQL structure
|
|
@@ -23,12 +21,6 @@ export declare class GraphQLService {
|
|
|
23
21
|
getSchema(): Promise<GraphQLSchema>;
|
|
24
22
|
getSchema(type: 'schema'): Promise<GraphQLSchema>;
|
|
25
23
|
getSchema(type: 'sdl'): Promise<GraphQLSchema | string>;
|
|
26
|
-
/**
|
|
27
|
-
* Generic resolver that's used for every "regular" items/system query. Converts the incoming GraphQL AST / fragments into
|
|
28
|
-
* Directus' query structure which is then executed by the services.
|
|
29
|
-
*/
|
|
30
|
-
resolveQuery(info: GraphQLResolveInfo): Promise<Partial<Item> | null>;
|
|
31
|
-
resolveMutation(args: Record<string, any>, info: GraphQLResolveInfo): Promise<Partial<Item> | boolean | undefined>;
|
|
32
24
|
/**
|
|
33
25
|
* Execute the read action on the correct service. Checks for singleton as well.
|
|
34
26
|
*/
|
|
@@ -37,44 +29,4 @@ export declare class GraphQLService {
|
|
|
37
29
|
* Upsert and read singleton item
|
|
38
30
|
*/
|
|
39
31
|
upsertSingleton(collection: string, body: Record<string, any> | Record<string, any>[], query: Query): Promise<Partial<Item> | boolean>;
|
|
40
|
-
/**
|
|
41
|
-
* GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
|
|
42
|
-
* whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
|
|
43
|
-
* arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
|
|
44
|
-
* In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
|
|
45
|
-
* of arguments
|
|
46
|
-
*/
|
|
47
|
-
parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any>;
|
|
48
|
-
/**
|
|
49
|
-
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
50
|
-
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
51
|
-
*/
|
|
52
|
-
getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues']): Query;
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the aggregation query based on the requested aggregated fields
|
|
55
|
-
*/
|
|
56
|
-
getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[]): Query;
|
|
57
|
-
/**
|
|
58
|
-
* Replace functions from GraphQL format to Directus-Filter format
|
|
59
|
-
*/
|
|
60
|
-
replaceFuncs(filter: Filter): Filter;
|
|
61
|
-
/**
|
|
62
|
-
* Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
|
|
63
|
-
*/
|
|
64
|
-
formatError(error: DirectusError | DirectusError[]): GraphQLError;
|
|
65
|
-
/**
|
|
66
|
-
* Replace all fragments in a selectionset for the actual selection set as defined in the fragment
|
|
67
|
-
* Effectively merges the selections with the fragments used in those selections
|
|
68
|
-
*/
|
|
69
|
-
replaceFragmentsInSelections(selections: readonly SelectionNode[] | undefined, fragments: Record<string, FragmentDefinitionNode>): readonly SelectionNode[] | null;
|
|
70
|
-
injectSystemResolvers(schemaComposer: SchemaComposer<GraphQLParams['contextValue']>, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, }: {
|
|
71
|
-
CreateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
72
|
-
ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
73
|
-
UpdateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
74
|
-
}, schema: {
|
|
75
|
-
create: SchemaOverview;
|
|
76
|
-
read: SchemaOverview;
|
|
77
|
-
update: SchemaOverview;
|
|
78
|
-
delete: SchemaOverview;
|
|
79
|
-
}): SchemaComposer<any>;
|
|
80
32
|
}
|