@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.
Files changed (85) hide show
  1. package/dist/app.js +10 -4
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cache.d.ts +2 -2
  5. package/dist/cache.js +20 -7
  6. package/dist/controllers/assets.js +2 -2
  7. package/dist/controllers/metrics.d.ts +2 -0
  8. package/dist/controllers/metrics.js +33 -0
  9. package/dist/controllers/server.js +1 -1
  10. package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
  11. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  12. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  13. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  14. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  15. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  16. package/dist/database/helpers/number/types.d.ts +2 -2
  17. package/dist/database/helpers/number/types.js +2 -2
  18. package/dist/database/index.js +3 -0
  19. package/dist/metrics/index.d.ts +1 -0
  20. package/dist/metrics/index.js +1 -0
  21. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  22. package/dist/metrics/lib/create-metrics.js +239 -0
  23. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  24. package/dist/metrics/lib/use-metrics.js +15 -0
  25. package/dist/metrics/types/metric.d.ts +1 -0
  26. package/dist/metrics/types/metric.js +1 -0
  27. package/dist/middleware/respond.js +7 -1
  28. package/dist/operations/condition/index.js +7 -2
  29. package/dist/schedules/metrics.d.ts +7 -0
  30. package/dist/schedules/metrics.js +44 -0
  31. package/dist/services/assets.d.ts +6 -1
  32. package/dist/services/assets.js +8 -6
  33. package/dist/services/fields.js +1 -1
  34. package/dist/services/graphql/errors/format.d.ts +6 -0
  35. package/dist/services/graphql/errors/format.js +14 -0
  36. package/dist/services/graphql/index.d.ts +5 -53
  37. package/dist/services/graphql/index.js +5 -2720
  38. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  39. package/dist/services/graphql/resolvers/mutation.js +74 -0
  40. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  41. package/dist/services/graphql/resolvers/query.js +87 -0
  42. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  43. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  44. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  45. package/dist/services/graphql/resolvers/system-global.js +435 -0
  46. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  47. package/dist/services/graphql/resolvers/system.js +554 -0
  48. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  49. package/dist/services/graphql/schema/get-types.js +223 -0
  50. package/dist/services/graphql/schema/index.d.ts +32 -0
  51. package/dist/services/graphql/schema/index.js +190 -0
  52. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  53. package/dist/services/graphql/schema/parse-args.js +35 -0
  54. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  55. package/dist/services/graphql/schema/parse-query.js +98 -0
  56. package/dist/services/graphql/schema/read.d.ts +12 -0
  57. package/dist/services/graphql/schema/read.js +653 -0
  58. package/dist/services/graphql/schema/write.d.ts +9 -0
  59. package/dist/services/graphql/schema/write.js +142 -0
  60. package/dist/services/graphql/subscription.d.ts +1 -1
  61. package/dist/services/graphql/subscription.js +7 -6
  62. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  63. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  64. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  65. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  66. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  67. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  68. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  69. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  70. package/dist/services/items.js +0 -2
  71. package/dist/services/meta.js +25 -84
  72. package/dist/services/users.d.ts +4 -0
  73. package/dist/services/users.js +23 -1
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/apply-query.js +58 -21
  76. package/dist/utils/freeze-schema.d.ts +3 -0
  77. package/dist/utils/freeze-schema.js +31 -0
  78. package/dist/utils/get-accountability-for-token.js +1 -0
  79. package/dist/utils/get-milliseconds.js +1 -1
  80. package/dist/utils/get-schema.js +10 -5
  81. package/dist/utils/permissions-cachable.d.ts +8 -0
  82. package/dist/utils/permissions-cachable.js +38 -0
  83. package/dist/utils/sanitize-schema.d.ts +1 -1
  84. package/dist/websocket/messages.d.ts +6 -6
  85. 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
- throw errors;
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,7 @@
1
+ export declare function handleMetricsJob(): Promise<void>;
2
+ /**
3
+ * Schedule the metric generation
4
+ *
5
+ * @returns Whether or not metrics has been initialized
6
+ */
7
+ export default function schedule(): Promise<boolean>;
@@ -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
  }
@@ -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: await storage.location(file.storage).read(assetFilename, { range }),
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: await storage.location(file.storage).read(assetFilename, { range, version }),
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 readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
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: readStream, file, stat };
166
+ return { stream: deferStream ? assetStream : await assetStream(), file, stat };
165
167
  }
166
168
  }
167
169
  }
@@ -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 { type DirectusError } from '@directus/errors';
2
- import type { Accountability, Filter, Item, Query, SchemaOverview } from '@directus/types';
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: 'items' | 'system';
10
+ scope: GQLScope;
13
11
  constructor(options: AbstractServiceOptions & {
14
- scope: 'items' | 'system';
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
  }