@directus/api 10.1.0 → 10.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.
@@ -117,7 +117,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
117
117
  if (userId) {
118
118
  // Run hook so the end user has the chance to augment the
119
119
  // user that is about to be updated
120
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
120
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data ?? null }, {
121
121
  identifier,
122
122
  provider: this.config['provider'],
123
123
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
@@ -136,7 +136,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
136
136
  if (userId) {
137
137
  // Run hook so the end user has the chance to augment the
138
138
  // user that is about to be updated
139
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
139
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data ?? null }, {
140
140
  identifier,
141
141
  provider: this.config['provider'],
142
142
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
@@ -43,6 +43,10 @@ PUBLIC_URL="/"
43
43
  # Whether or not to enable GraphQL Introspection [true]
44
44
  # GRAPHQL_INTROSPECTION=true
45
45
 
46
+ # Limit the maximum amount of items that can get requested in one query.
47
+ # QUERY_LIMIT_DEFAULT=100
48
+ # QUERY_LIMIT_MAX=Infinity
49
+
46
50
  # The maximum number of items for batch mutations when creating, updating and deleting. ["Infinity"]
47
51
  # MAX_BATCH_MUTATION="Infinity"
48
52
 
@@ -106,21 +106,17 @@ asyncHandler(async (req, res) => {
106
106
  schema: req.schema,
107
107
  });
108
108
  const vary = ['Origin', 'Cache-Control'];
109
- const transformation = res.locals['transformation'].key
109
+ const transformationParams = res.locals['transformation'].key
110
110
  ? res.locals['shortcuts'].find((transformation) => transformation['key'] === res.locals['transformation'].key)
111
111
  : res.locals['transformation'];
112
- if (transformation.format === 'auto') {
113
- let format;
112
+ let acceptFormat;
113
+ if (transformationParams.format === 'auto') {
114
114
  if (req.headers.accept?.includes('image/avif')) {
115
- format = 'avif';
115
+ acceptFormat = 'avif';
116
116
  }
117
117
  else if (req.headers.accept?.includes('image/webp')) {
118
- format = 'webp';
118
+ acceptFormat = 'webp';
119
119
  }
120
- else {
121
- format = 'jpg';
122
- }
123
- transformation.format = format;
124
120
  vary.push('Accept');
125
121
  }
126
122
  let range = undefined;
@@ -140,7 +136,7 @@ asyncHandler(async (req, res) => {
140
136
  }
141
137
  }
142
138
  }
143
- const { stream, file, stat } = await service.getAsset(id, transformation, range);
139
+ const { stream, file, stat } = await service.getAsset(id, { transformationParams, acceptFormat }, range);
144
140
  const filename = req.params['filename'] ?? file.filename_download;
145
141
  res.attachment(filename);
146
142
  res.setHeader('Content-Type', file.type);
@@ -151,7 +151,7 @@ async function getDBQuery(schema, knex, table, fieldNodes, query) {
151
151
  const preProcess = getColumnPreprocessor(knex, schema, table);
152
152
  const queryCopy = clone(query);
153
153
  const helpers = getHelpers(knex);
154
- queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : 100;
154
+ queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : Number(env['QUERY_LIMIT_DEFAULT']);
155
155
  // Queries with aggregates and groupBy will not have duplicate result
156
156
  if (queryCopy.aggregate || queryCopy.group) {
157
157
  const flatQuery = knex.select(fieldNodes.map(preProcess)).from(table);
@@ -322,13 +322,13 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode) {
322
322
  });
323
323
  parentItem[nestedNode.fieldKey].push(...itemChildren);
324
324
  if (nestedNode.query.page && nestedNode.query.page > 1) {
325
- parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice((nestedNode.query.limit ?? 100) * (nestedNode.query.page - 1));
325
+ parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice((nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT'])) * (nestedNode.query.page - 1));
326
326
  }
327
327
  if (nestedNode.query.offset && nestedNode.query.offset >= 0) {
328
328
  parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(nestedNode.query.offset);
329
329
  }
330
330
  if (nestedNode.query.limit !== -1) {
331
- parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, nestedNode.query.limit ?? 100);
331
+ parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT']));
332
332
  }
333
333
  parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].sort((a, b) => {
334
334
  // This is pre-filled in get-ast-from-query
package/dist/env.js CHANGED
@@ -25,6 +25,8 @@ const allowedEnvironmentVars = [
25
25
  'GRAPHQL_INTROSPECTION',
26
26
  'MAX_BATCH_MUTATION',
27
27
  'LOGGER_.+',
28
+ 'QUERY_LIMIT_MAX',
29
+ 'QUERY_LIMIT_DEFAULT',
28
30
  'ROBOTS_TXT',
29
31
  // server
30
32
  'SERVER_.+',
@@ -196,6 +198,7 @@ const defaults = {
196
198
  PUBLIC_URL: '/',
197
199
  MAX_PAYLOAD_SIZE: '1mb',
198
200
  MAX_RELATIONAL_DEPTH: 10,
201
+ QUERY_LIMIT_DEFAULT: 100,
199
202
  MAX_BATCH_MUTATION: Infinity,
200
203
  ROBOTS_TXT: 'User-agent: *\nDisallow: /',
201
204
  DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',
@@ -3,14 +3,14 @@ import type { Range, Stat } from '@directus/storage';
3
3
  import type { Accountability } from '@directus/types';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Readable } from 'node:stream';
6
- import type { AbstractServiceOptions, TransformationParams } from '../types/index.js';
6
+ import type { AbstractServiceOptions, TransformationSet } from '../types/index.js';
7
7
  import { AuthorizationService } from './authorization.js';
8
8
  export declare class AssetsService {
9
9
  knex: Knex;
10
10
  accountability: Accountability | null;
11
11
  authorizationService: AuthorizationService;
12
12
  constructor(options: AbstractServiceOptions);
13
- getAsset(id: string, transformation: TransformationParams, range?: Range): Promise<{
13
+ getAsset(id: string, transformation: TransformationSet, range?: Range): Promise<{
14
14
  stream: Readable;
15
15
  file: any;
16
16
  stat: Stat;
@@ -311,7 +311,9 @@ export class FieldsService {
311
311
  }
312
312
  if (hookAdjustedField.schema) {
313
313
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
314
- if (!isEqual(sanitizeColumn(existingColumn), hookAdjustedField.schema)) {
314
+ // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
315
+ const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
316
+ if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
315
317
  try {
316
318
  await this.knex.schema.alterTable(collection, (table) => {
317
319
  if (!hookAdjustedField.schema)
@@ -1539,6 +1539,15 @@ export class GraphQLService {
1539
1539
  },
1540
1540
  }),
1541
1541
  },
1542
+ queryLimit: {
1543
+ type: new GraphQLObjectType({
1544
+ name: 'server_info_query_limit',
1545
+ fields: {
1546
+ default: { type: GraphQLInt },
1547
+ max: { type: GraphQLInt },
1548
+ },
1549
+ }),
1550
+ },
1542
1551
  });
1543
1552
  }
1544
1553
  if (this.accountability?.admin === true) {
@@ -64,6 +64,10 @@ export class ServerService {
64
64
  info['flows'] = {
65
65
  execAllowedModules: env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [],
66
66
  };
67
+ info['queryLimit'] = {
68
+ default: env['QUERY_LIMIT_DEFAULT'],
69
+ max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
70
+ };
67
71
  }
68
72
  if (this.accountability?.admin === true) {
69
73
  const { osType, osVersion } = getOSInfo();
@@ -6,10 +6,15 @@ export type TransformationMap = {
6
6
  };
7
7
  export type Transformation = TransformationMap[keyof TransformationMap];
8
8
  export type TransformationResize = Pick<ResizeOptions, 'width' | 'height' | 'fit' | 'withoutEnlargement'>;
9
+ export type TransformationFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'tiff' | 'avif';
9
10
  export type TransformationParams = {
10
11
  key?: string;
11
12
  transforms?: Transformation[];
12
- format?: 'auto' | 'jpg' | 'jpeg' | 'png' | 'webp' | 'tiff' | 'avif';
13
+ format?: TransformationFormat | 'auto';
13
14
  quality?: number;
14
15
  } & TransformationResize;
16
+ export type TransformationSet = {
17
+ transformationParams: TransformationParams;
18
+ acceptFormat?: TransformationFormat | undefined;
19
+ };
15
20
  export {};
@@ -198,7 +198,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
198
198
  }
199
199
  const filterPath = getFilterPath(key, value);
200
200
  if (filterPath.length > 1 ||
201
- (!(key.includes('(') && key.includes(')')) && schema.collections[collection].fields[key].type === 'alias')) {
201
+ (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
202
202
  const hasMultiRelational = addJoin({
203
203
  path: filterPath,
204
204
  collection,
@@ -238,7 +238,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
238
238
  const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
239
239
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
240
240
  if (filterPath.length > 1 ||
241
- (!(key.includes('(') && key.includes(')')) && schema.collections[collection].fields[key].type === 'alias')) {
241
+ (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
242
242
  if (!relation)
243
243
  continue;
244
244
  if (relationType === 'o2m' || relationType === 'o2a') {
@@ -1,15 +1,24 @@
1
1
  import { parseFilter, parseJSON } from '@directus/utils';
2
2
  import { flatten, get, isPlainObject, merge, set } from 'lodash-es';
3
+ import { getEnv } from '../env.js';
3
4
  import logger from '../logger.js';
4
5
  import { Meta } from '../types/index.js';
5
6
  export function sanitizeQuery(rawQuery, accountability) {
6
7
  const query = {};
8
+ const env = getEnv();
9
+ const hasMaxLimit = 'QUERY_LIMIT_MAX' in env &&
10
+ Number(env['QUERY_LIMIT_MAX']) >= 0 &&
11
+ !Number.isNaN(Number(env['QUERY_LIMIT_MAX'])) &&
12
+ Number.isFinite(Number(env['QUERY_LIMIT_MAX']));
7
13
  if (rawQuery['limit'] !== undefined) {
8
14
  const limit = sanitizeLimit(rawQuery['limit']);
9
15
  if (typeof limit === 'number') {
10
- query.limit = limit;
16
+ query.limit = limit === -1 && hasMaxLimit ? Number(env['QUERY_LIMIT_MAX']) : limit;
11
17
  }
12
18
  }
19
+ else if (hasMaxLimit) {
20
+ query.limit = Math.min(Number(env['QUERY_LIMIT_DEFAULT']), Number(env['QUERY_LIMIT_MAX']));
21
+ }
13
22
  if (rawQuery['fields']) {
14
23
  query.fields = sanitizeFields(rawQuery['fields']);
15
24
  }
@@ -1,5 +1,5 @@
1
- import type { File, Transformation, TransformationParams } from '../types/index.js';
2
- export declare function resolvePreset(input: TransformationParams, file: File): Transformation[];
1
+ import type { File, Transformation, TransformationSet } from '../types/index.js';
2
+ export declare function resolvePreset({ transformationParams, acceptFormat }: TransformationSet, file: File): Transformation[];
3
3
  /**
4
4
  * Try to extract a file format from an array of `Transformation`'s.
5
5
  */
@@ -1,27 +1,44 @@
1
- export function resolvePreset(input, file) {
2
- const transforms = input.transforms ? [...input.transforms] : [];
3
- if (input.format || input.quality) {
1
+ export function resolvePreset({ transformationParams, acceptFormat }, file) {
2
+ const transforms = transformationParams.transforms ? [...transformationParams.transforms] : [];
3
+ if (transformationParams.format || transformationParams.quality) {
4
4
  transforms.push([
5
5
  'toFormat',
6
- input.format || file.type.split('/')[1],
6
+ getFormat(file, transformationParams.format, acceptFormat),
7
7
  {
8
- quality: input.quality ? Number(input.quality) : undefined,
8
+ quality: transformationParams.quality ? Number(transformationParams.quality) : undefined,
9
9
  },
10
10
  ]);
11
11
  }
12
- if (input.width || input.height) {
12
+ if (transformationParams.width || transformationParams.height) {
13
13
  transforms.push([
14
14
  'resize',
15
15
  {
16
- width: input.width ? Number(input.width) : undefined,
17
- height: input.height ? Number(input.height) : undefined,
18
- fit: input.fit,
19
- withoutEnlargement: input.withoutEnlargement ? Boolean(input.withoutEnlargement) : undefined,
16
+ width: transformationParams.width ? Number(transformationParams.width) : undefined,
17
+ height: transformationParams.height ? Number(transformationParams.height) : undefined,
18
+ fit: transformationParams.fit,
19
+ withoutEnlargement: transformationParams.withoutEnlargement
20
+ ? Boolean(transformationParams.withoutEnlargement)
21
+ : undefined,
20
22
  },
21
23
  ]);
22
24
  }
23
25
  return transforms;
24
26
  }
27
+ function getFormat(file, format, acceptFormat) {
28
+ const fileType = file.type?.split('/')[1];
29
+ if (format) {
30
+ if (format !== 'auto') {
31
+ return format;
32
+ }
33
+ if (acceptFormat) {
34
+ return acceptFormat;
35
+ }
36
+ if (fileType && ['avif', 'webp', 'tiff'].includes(fileType)) {
37
+ return 'png';
38
+ }
39
+ }
40
+ return fileType || 'jpg';
41
+ }
25
42
  /**
26
43
  * Try to extract a file format from an array of `Transformation`'s.
27
44
  */
@@ -9,7 +9,9 @@ const querySchema = Joi.object({
9
9
  group: Joi.array().items(Joi.string()),
10
10
  sort: Joi.array().items(Joi.string()),
11
11
  filter: Joi.object({}).unknown(),
12
- limit: Joi.number().integer().min(-1),
12
+ limit: 'QUERY_LIMIT_MAX' in env && env['QUERY_LIMIT_MAX'] !== -1
13
+ ? Joi.number().integer().min(-1).max(env['QUERY_LIMIT_MAX']) // min should be 0
14
+ : Joi.number().integer().min(-1),
13
15
  offset: Joi.number().integer().min(0),
14
16
  page: Joi.number().integer().min(0),
15
17
  meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -139,23 +139,23 @@
139
139
  "tsx": "3.12.6",
140
140
  "uuid": "9.0.0",
141
141
  "uuid-validate": "0.0.3",
142
- "vm2": "3.9.17",
142
+ "vm2": "3.9.18",
143
143
  "wellknown": "0.5.0",
144
- "@directus/app": "10.1.0",
144
+ "@directus/app": "10.2.0",
145
145
  "@directus/constants": "10.1.0",
146
146
  "@directus/exceptions": "10.0.1",
147
- "@directus/extensions-sdk": "10.1.0",
148
- "@directus/pressure": "1.0.0",
149
- "@directus/schema": "10.0.0",
147
+ "@directus/extensions-sdk": "10.1.1",
148
+ "@directus/pressure": "1.0.1",
149
+ "@directus/schema": "10.0.1",
150
150
  "@directus/specs": "10.1.0",
151
151
  "@directus/storage": "10.0.1",
152
- "@directus/storage-driver-azure": "10.0.1",
153
- "@directus/storage-driver-cloudinary": "10.0.1",
154
- "@directus/storage-driver-gcs": "10.0.1",
155
- "@directus/storage-driver-local": "10.0.1",
156
- "@directus/storage-driver-s3": "10.0.1",
152
+ "@directus/storage-driver-azure": "10.0.2",
153
+ "@directus/storage-driver-cloudinary": "10.0.2",
154
+ "@directus/storage-driver-gcs": "10.0.2",
155
+ "@directus/storage-driver-local": "10.0.2",
156
+ "@directus/storage-driver-s3": "10.0.2",
157
157
  "@directus/update-check": "10.0.1",
158
- "@directus/utils": "10.0.1"
158
+ "@directus/utils": "10.0.2"
159
159
  },
160
160
  "devDependencies": {
161
161
  "@directus/tsconfig": "0.0.7",
@@ -203,7 +203,7 @@
203
203
  "supertest": "6.3.3",
204
204
  "typescript": "5.0.4",
205
205
  "vitest": "0.31.0",
206
- "@directus/types": "10.0.0"
206
+ "@directus/types": "10.0.1"
207
207
  },
208
208
  "optionalDependencies": {
209
209
  "@keyv/redis": "2.5.7",