@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.
- package/dist/auth/drivers/oauth2.js +1 -1
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +4 -0
- package/dist/controllers/assets.js +6 -10
- package/dist/database/run-ast.js +3 -3
- package/dist/env.js +3 -0
- package/dist/services/assets.d.ts +2 -2
- package/dist/services/fields.js +3 -1
- package/dist/services/graphql/index.js +9 -0
- package/dist/services/server.js +4 -0
- package/dist/types/assets.d.ts +6 -1
- package/dist/utils/apply-query.js +2 -2
- package/dist/utils/sanitize-query.js +10 -1
- package/dist/utils/transformations.d.ts +2 -2
- package/dist/utils/transformations.js +27 -10
- package/dist/utils/validate-query.js +3 -1
- package/package.json +13 -13
|
@@ -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
|
|
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
|
-
|
|
113
|
-
|
|
112
|
+
let acceptFormat;
|
|
113
|
+
if (transformationParams.format === 'auto') {
|
|
114
114
|
if (req.headers.accept?.includes('image/avif')) {
|
|
115
|
-
|
|
115
|
+
acceptFormat = 'avif';
|
|
116
116
|
}
|
|
117
117
|
else if (req.headers.accept?.includes('image/webp')) {
|
|
118
|
-
|
|
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,
|
|
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);
|
package/dist/database/run-ast.js
CHANGED
|
@@ -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 :
|
|
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 ??
|
|
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 ??
|
|
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,
|
|
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:
|
|
13
|
+
getAsset(id: string, transformation: TransformationSet, range?: Range): Promise<{
|
|
14
14
|
stream: Readable;
|
|
15
15
|
file: any;
|
|
16
16
|
stat: Stat;
|
package/dist/services/fields.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/services/server.js
CHANGED
|
@@ -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();
|
package/dist/types/assets.d.ts
CHANGED
|
@@ -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?:
|
|
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]
|
|
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]
|
|
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,
|
|
2
|
-
export declare function resolvePreset(
|
|
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(
|
|
2
|
-
const transforms =
|
|
3
|
-
if (
|
|
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
|
-
|
|
6
|
+
getFormat(file, transformationParams.format, acceptFormat),
|
|
7
7
|
{
|
|
8
|
-
quality:
|
|
8
|
+
quality: transformationParams.quality ? Number(transformationParams.quality) : undefined,
|
|
9
9
|
},
|
|
10
10
|
]);
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
12
|
+
if (transformationParams.width || transformationParams.height) {
|
|
13
13
|
transforms.push([
|
|
14
14
|
'resize',
|
|
15
15
|
{
|
|
16
|
-
width:
|
|
17
|
-
height:
|
|
18
|
-
fit:
|
|
19
|
-
withoutEnlargement:
|
|
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:
|
|
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.
|
|
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.
|
|
142
|
+
"vm2": "3.9.18",
|
|
143
143
|
"wellknown": "0.5.0",
|
|
144
|
-
"@directus/app": "10.
|
|
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.
|
|
148
|
-
"@directus/pressure": "1.0.
|
|
149
|
-
"@directus/schema": "10.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.
|
|
153
|
-
"@directus/storage-driver-cloudinary": "10.0.
|
|
154
|
-
"@directus/storage-driver-gcs": "10.0.
|
|
155
|
-
"@directus/storage-driver-local": "10.0.
|
|
156
|
-
"@directus/storage-driver-s3": "10.0.
|
|
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.
|
|
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.
|
|
206
|
+
"@directus/types": "10.0.1"
|
|
207
207
|
},
|
|
208
208
|
"optionalDependencies": {
|
|
209
209
|
"@keyv/redis": "2.5.7",
|