@directus/api 19.0.1 → 19.1.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 +8 -7
- package/dist/auth/drivers/oauth2.js +3 -2
- package/dist/auth/drivers/openid.js +3 -2
- package/dist/cli/utils/create-env/env-stub.liquid +0 -3
- package/dist/cli/utils/create-env/index.js +0 -2
- package/dist/controllers/auth.js +3 -2
- package/dist/controllers/extensions.js +30 -19
- package/dist/controllers/users.js +25 -0
- package/dist/controllers/utils.js +2 -1
- package/dist/database/helpers/fn/types.js +4 -3
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/number/dialects/default.d.ts +3 -0
- package/dist/database/helpers/number/dialects/default.js +3 -0
- package/dist/database/helpers/number/dialects/mssql.d.ts +7 -0
- package/dist/database/helpers/number/dialects/mssql.js +11 -0
- package/dist/database/helpers/number/dialects/oracle.d.ts +6 -0
- package/dist/database/helpers/number/dialects/oracle.js +7 -0
- package/dist/database/helpers/number/dialects/postgres.d.ts +5 -0
- package/dist/database/helpers/number/dialects/postgres.js +15 -0
- package/dist/database/helpers/number/dialects/sqlite.d.ts +6 -0
- package/dist/database/helpers/number/dialects/sqlite.js +7 -0
- package/dist/database/helpers/number/index.d.ts +7 -0
- package/dist/database/helpers/number/index.js +7 -0
- package/dist/database/helpers/number/types.d.ts +12 -0
- package/dist/database/helpers/number/types.js +9 -0
- package/dist/database/helpers/number/utils/decimal-limit.d.ts +4 -0
- package/dist/database/helpers/number/utils/decimal-limit.js +10 -0
- package/dist/database/helpers/number/utils/maybe-stringify-big-int.d.ts +1 -0
- package/dist/database/helpers/number/utils/maybe-stringify-big-int.js +6 -0
- package/dist/database/helpers/number/utils/number-in-range.d.ts +3 -0
- package/dist/database/helpers/number/utils/number-in-range.js +20 -0
- package/dist/database/migrations/20240422A-public-registration.d.ts +3 -0
- package/dist/database/migrations/20240422A-public-registration.js +14 -0
- package/dist/database/run-ast.js +5 -4
- package/dist/extensions/lib/get-extensions-settings.js +48 -11
- package/dist/extensions/lib/installation/manager.js +2 -2
- package/dist/middleware/rate-limiter-global.js +1 -1
- package/dist/middleware/rate-limiter-registration.d.ts +5 -0
- package/dist/middleware/rate-limiter-registration.js +32 -0
- package/dist/services/authentication.js +3 -2
- package/dist/services/authorization.js +4 -4
- package/dist/services/fields.js +2 -2
- package/dist/services/graphql/index.js +41 -2
- package/dist/services/mail/templates/user-registration.liquid +37 -0
- package/dist/services/meta.js +1 -1
- package/dist/services/payload.d.ts +2 -0
- package/dist/services/payload.js +16 -4
- package/dist/services/server.js +3 -1
- package/dist/services/shares.js +2 -1
- package/dist/services/users.d.ts +3 -1
- package/dist/services/users.js +92 -5
- package/dist/services/utils.d.ts +3 -1
- package/dist/services/utils.js +7 -3
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +54 -28
- package/dist/utils/get-accountability-for-token.js +6 -3
- package/dist/utils/get-cache-headers.js +3 -0
- package/dist/utils/get-schema.js +21 -12
- package/dist/utils/get-secret.d.ts +4 -0
- package/dist/utils/get-secret.js +14 -0
- package/dist/utils/parse-filter-key.d.ts +7 -0
- package/dist/utils/parse-filter-key.js +22 -0
- package/dist/utils/parse-numeric-string.d.ts +2 -0
- package/dist/utils/parse-numeric-string.js +21 -0
- package/dist/utils/sanitize-query.js +10 -5
- package/dist/utils/transaction.d.ts +1 -1
- package/dist/utils/transaction.js +39 -2
- package/dist/utils/validate-query.js +0 -2
- package/dist/utils/verify-session-jwt.d.ts +7 -0
- package/dist/utils/verify-session-jwt.js +22 -0
- package/dist/websocket/messages.d.ts +78 -50
- package/package.json +60 -61
- package/dist/utils/strip-function.d.ts +0 -4
- package/dist/utils/strip-function.js +0 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { NUMERIC_TYPES } from '@directus/constants';
|
|
1
2
|
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
-
import { getFilterOperatorsForType, getOutputTypeForFunction } from '@directus/utils';
|
|
3
|
+
import { getFilterOperatorsForType, getFunctionsForType, getOutputTypeForFunction, isIn } from '@directus/utils';
|
|
3
4
|
import { clone, isPlainObject } from 'lodash-es';
|
|
4
5
|
import { customAlphabet } from 'nanoid/non-secure';
|
|
5
6
|
import { getHelpers } from '../database/helpers/index.js';
|
|
@@ -7,7 +8,8 @@ import { getColumnPath } from './get-column-path.js';
|
|
|
7
8
|
import { getColumn } from './get-column.js';
|
|
8
9
|
import { getRelationInfo } from './get-relation-info.js';
|
|
9
10
|
import { isValidUuid } from './is-valid-uuid.js';
|
|
10
|
-
import {
|
|
11
|
+
import { parseFilterKey } from './parse-filter-key.js';
|
|
12
|
+
import { parseNumericString } from './parse-numeric-string.js';
|
|
11
13
|
export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
|
|
12
14
|
/**
|
|
13
15
|
* Apply the Query to a given Knex query builder instance
|
|
@@ -30,7 +32,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
if (query.search) {
|
|
33
|
-
applySearch(schema, dbQuery, query.search, collection);
|
|
35
|
+
applySearch(knex, schema, dbQuery, query.search, collection);
|
|
34
36
|
}
|
|
35
37
|
if (query.group) {
|
|
36
38
|
dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
|
|
@@ -84,7 +86,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
84
86
|
}
|
|
85
87
|
rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
|
|
86
88
|
joinClause
|
|
87
|
-
.onVal(relation.meta.one_collection_field
|
|
89
|
+
.onVal(`${aliasedParentCollection}.${relation.meta.one_collection_field}`, '=', pathScope)
|
|
88
90
|
.andOn(`${aliasedParentCollection}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${alias}.${schema.collections[pathScope].primary}`));
|
|
89
91
|
});
|
|
90
92
|
aliasMap[aliasKey].collection = pathScope;
|
|
@@ -93,7 +95,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
93
95
|
else if (relationType === 'o2a') {
|
|
94
96
|
rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
|
|
95
97
|
joinClause
|
|
96
|
-
.onVal(relation.meta.one_collection_field
|
|
98
|
+
.onVal(`${alias}.${relation.meta.one_collection_field}`, '=', parentCollection)
|
|
97
99
|
.andOn(`${alias}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${aliasedParentCollection}.${schema.collections[parentCollection].primary}`));
|
|
98
100
|
});
|
|
99
101
|
aliasMap[aliasKey].collection = relation.collection;
|
|
@@ -288,7 +290,10 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
288
290
|
*/
|
|
289
291
|
const pathRoot = filterPath[0].split(':')[0];
|
|
290
292
|
const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
|
|
291
|
-
const
|
|
293
|
+
const operation = getOperation(key, value);
|
|
294
|
+
if (!operation)
|
|
295
|
+
continue;
|
|
296
|
+
const { operator: filterOperator, value: filterValue } = operation;
|
|
292
297
|
if (filterPath.length > 1 ||
|
|
293
298
|
(!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
|
|
294
299
|
if (!relation)
|
|
@@ -335,21 +340,32 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
335
340
|
}
|
|
336
341
|
if (!columnPath)
|
|
337
342
|
continue;
|
|
338
|
-
const { type, special } =
|
|
343
|
+
const { type, special } = getFilterType(schema.collections[targetCollection].fields, filterPath.at(-1), targetCollection);
|
|
339
344
|
validateFilterOperator(type, filterOperator, special);
|
|
340
345
|
applyFilterToQuery(columnPath, filterOperator, filterValue, logical, targetCollection);
|
|
341
346
|
}
|
|
342
347
|
else {
|
|
343
|
-
const { type, special } =
|
|
348
|
+
const { type, special } = getFilterType(schema.collections[collection].fields, filterPath[0], collection);
|
|
344
349
|
validateFilterOperator(type, filterOperator, special);
|
|
345
350
|
applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue, logical);
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
353
|
+
function getFilterType(fields, key, collection = 'unknown') {
|
|
354
|
+
const { fieldName, functionName } = parseFilterKey(key);
|
|
355
|
+
const field = fields[fieldName];
|
|
356
|
+
if (!field) {
|
|
350
357
|
throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
|
|
351
358
|
}
|
|
352
|
-
|
|
359
|
+
const { type } = field;
|
|
360
|
+
if (functionName) {
|
|
361
|
+
const availableFunctions = getFunctionsForType(type);
|
|
362
|
+
if (!availableFunctions.includes(functionName)) {
|
|
363
|
+
throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
|
|
364
|
+
}
|
|
365
|
+
const functionType = getOutputTypeForFunction(functionName);
|
|
366
|
+
return { type: functionType };
|
|
367
|
+
}
|
|
368
|
+
return { type, special: field.special };
|
|
353
369
|
}
|
|
354
370
|
function validateFilterOperator(type, filterOperator, special) {
|
|
355
371
|
if (filterOperator.startsWith('_')) {
|
|
@@ -360,7 +376,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
360
376
|
reason: `"${type}" field type does not contain the "_${filterOperator}" filter operator`,
|
|
361
377
|
});
|
|
362
378
|
}
|
|
363
|
-
if (special
|
|
379
|
+
if (special?.includes('conceal') &&
|
|
364
380
|
!getFilterOperatorsForType('hash').includes(filterOperator)) {
|
|
365
381
|
throw new InvalidQueryError({
|
|
366
382
|
reason: `Field with "conceal" special does not allow the "_${filterOperator}" filter operator`,
|
|
@@ -534,33 +550,36 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
534
550
|
}
|
|
535
551
|
}
|
|
536
552
|
}
|
|
537
|
-
export async function applySearch(schema, dbQuery, searchQuery, collection) {
|
|
553
|
+
export async function applySearch(knex, schema, dbQuery, searchQuery, collection) {
|
|
554
|
+
const { number: numberHelper } = getHelpers(knex);
|
|
538
555
|
const fields = Object.entries(schema.collections[collection].fields);
|
|
539
556
|
dbQuery.andWhere(function () {
|
|
557
|
+
let needsFallbackCondition = true;
|
|
540
558
|
fields.forEach(([name, field]) => {
|
|
541
559
|
if (['text', 'string'].includes(field.type)) {
|
|
542
560
|
this.orWhereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
|
|
561
|
+
needsFallbackCondition = false;
|
|
543
562
|
}
|
|
544
|
-
else if (
|
|
545
|
-
const number =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
563
|
+
else if (isNumericField(field)) {
|
|
564
|
+
const number = parseNumericString(searchQuery);
|
|
565
|
+
if (number === null) {
|
|
566
|
+
return; // unable to parse
|
|
567
|
+
}
|
|
568
|
+
if (numberHelper.isNumberValid(number, field)) {
|
|
569
|
+
numberHelper.addSearchCondition(this, collection, name, number);
|
|
570
|
+
needsFallbackCondition = false;
|
|
549
571
|
}
|
|
550
572
|
}
|
|
551
573
|
else if (field.type === 'uuid' && isValidUuid(searchQuery)) {
|
|
552
574
|
this.orWhere({ [`${collection}.${name}`]: searchQuery });
|
|
575
|
+
needsFallbackCondition = false;
|
|
553
576
|
}
|
|
554
577
|
});
|
|
578
|
+
if (needsFallbackCondition) {
|
|
579
|
+
this.orWhereRaw('1 = 0');
|
|
580
|
+
}
|
|
555
581
|
});
|
|
556
582
|
}
|
|
557
|
-
function validateNumber(value, parsed) {
|
|
558
|
-
if (isNaN(parsed) || !Number.isFinite(parsed))
|
|
559
|
-
return false;
|
|
560
|
-
// casting parsed value back to string should be equal the original value
|
|
561
|
-
// (prevent unintended number parsing, e.g. String(7) !== "ob111")
|
|
562
|
-
return String(parsed) === value;
|
|
563
|
-
}
|
|
564
583
|
export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins) {
|
|
565
584
|
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
566
585
|
if (!fields)
|
|
@@ -610,7 +629,7 @@ export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins)
|
|
|
610
629
|
function getFilterPath(key, value) {
|
|
611
630
|
const path = [key];
|
|
612
631
|
const childKey = Object.keys(value)[0];
|
|
613
|
-
if (
|
|
632
|
+
if (!childKey || (childKey.startsWith('_') === true && !['_none', '_some'].includes(childKey))) {
|
|
614
633
|
return path;
|
|
615
634
|
}
|
|
616
635
|
if (isPlainObject(value)) {
|
|
@@ -622,8 +641,15 @@ function getOperation(key, value) {
|
|
|
622
641
|
if (key.startsWith('_') && !['_and', '_or', '_none', '_some'].includes(key)) {
|
|
623
642
|
return { operator: key, value };
|
|
624
643
|
}
|
|
625
|
-
else if (isPlainObject(value)
|
|
644
|
+
else if (!isPlainObject(value)) {
|
|
626
645
|
return { operator: '_eq', value };
|
|
627
646
|
}
|
|
628
|
-
|
|
647
|
+
const childKey = Object.keys(value)[0];
|
|
648
|
+
if (childKey) {
|
|
649
|
+
return getOperation(childKey, Object.values(value)[0]);
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
function isNumericField(field) {
|
|
654
|
+
return isIn(field.type, NUMERIC_TYPES);
|
|
629
655
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useEnv } from '@directus/env';
|
|
2
1
|
import { InvalidCredentialsError } from '@directus/errors';
|
|
3
2
|
import getDatabase from '../database/index.js';
|
|
3
|
+
import { getSecret } from './get-secret.js';
|
|
4
4
|
import isDirectusJWT from './is-directus-jwt.js';
|
|
5
|
+
import { verifySessionJWT } from './verify-session-jwt.js';
|
|
5
6
|
import { verifyAccessJWT } from './jwt.js';
|
|
6
7
|
export async function getAccountabilityForToken(token, accountability) {
|
|
7
|
-
const env = useEnv();
|
|
8
8
|
if (!accountability) {
|
|
9
9
|
accountability = {
|
|
10
10
|
user: null,
|
|
@@ -15,7 +15,10 @@ export async function getAccountabilityForToken(token, accountability) {
|
|
|
15
15
|
}
|
|
16
16
|
if (token) {
|
|
17
17
|
if (isDirectusJWT(token)) {
|
|
18
|
-
const payload = verifyAccessJWT(token,
|
|
18
|
+
const payload = verifyAccessJWT(token, getSecret());
|
|
19
|
+
if ('session' in payload) {
|
|
20
|
+
await verifySessionJWT(payload);
|
|
21
|
+
}
|
|
19
22
|
accountability.role = payload.role;
|
|
20
23
|
accountability.admin = payload.admin_access === true || payload.admin_access == 1;
|
|
21
24
|
accountability.app = payload.app_access === true || payload.app_access == 1;
|
|
@@ -16,6 +16,9 @@ export function getCacheControlHeader(req, ttl, globalCacheSettings, personalize
|
|
|
16
16
|
// When the resource / current request shouldn't be cached
|
|
17
17
|
if (ttl === undefined || ttl < 0)
|
|
18
18
|
return 'no-cache';
|
|
19
|
+
// When the API cache can invalidate at any moment
|
|
20
|
+
if (globalCacheSettings && env['CACHE_AUTO_PURGE'] === true)
|
|
21
|
+
return 'no-cache';
|
|
19
22
|
const headerValues = [];
|
|
20
23
|
// When caching depends on the authentication status of the users
|
|
21
24
|
if (personalized) {
|
package/dist/utils/get-schema.js
CHANGED
|
@@ -34,24 +34,33 @@ export async function getSchema(options, attempt = 0) {
|
|
|
34
34
|
const lockKey = 'schemaCache--preparing';
|
|
35
35
|
const messageKey = 'schemaCache--done';
|
|
36
36
|
const processId = await lock.increment(lockKey);
|
|
37
|
+
if (processId >= env['CACHE_SCHEMA_MAX_ITERATIONS']) {
|
|
38
|
+
await lock.delete(lockKey);
|
|
39
|
+
}
|
|
37
40
|
const currentProcessShouldHandleOperation = processId === 1;
|
|
38
41
|
if (currentProcessShouldHandleOperation === false) {
|
|
39
42
|
logger.trace('Schema cache is prepared in another process, waiting for result.');
|
|
40
|
-
return new Promise((resolve) => {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
41
44
|
const TIMEOUT = 10000;
|
|
42
|
-
|
|
43
|
-
const callback = async () => {
|
|
44
|
-
if (timeout)
|
|
45
|
-
clearTimeout(timeout);
|
|
46
|
-
const schema = await getSchema(options, attempt + 1);
|
|
47
|
-
resolve(schema);
|
|
48
|
-
bus.unsubscribe(messageKey, callback);
|
|
49
|
-
};
|
|
50
|
-
bus.subscribe(messageKey, callback);
|
|
51
|
-
timeout = setTimeout(async () => {
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
52
46
|
logger.trace('Did not receive schema callback message in time. Pulling schema...');
|
|
53
|
-
callback();
|
|
47
|
+
callback().catch(reject);
|
|
54
48
|
}, TIMEOUT);
|
|
49
|
+
bus.subscribe(messageKey, callback);
|
|
50
|
+
async function callback() {
|
|
51
|
+
try {
|
|
52
|
+
if (timeout)
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
const schema = await getSchema(options, attempt + 1);
|
|
55
|
+
resolve(schema);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
reject(error);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
bus.unsubscribe(messageKey, callback);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
55
64
|
});
|
|
56
65
|
}
|
|
57
66
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
export const _cache = { secret: null };
|
|
4
|
+
export const getSecret = () => {
|
|
5
|
+
if (_cache.secret) {
|
|
6
|
+
return _cache.secret;
|
|
7
|
+
}
|
|
8
|
+
const env = useEnv();
|
|
9
|
+
if (env['SECRET']) {
|
|
10
|
+
return env['SECRET'];
|
|
11
|
+
}
|
|
12
|
+
_cache.secret = nanoid(32);
|
|
13
|
+
return _cache.secret;
|
|
14
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result for keys with a function (e.g. `year(date_created)`):
|
|
3
|
+
* - Group 1: Function (`year`)
|
|
4
|
+
* - Group 3: Field (`date_created`)
|
|
5
|
+
*
|
|
6
|
+
* If group 3 is undefined, it is a key without a function.
|
|
7
|
+
*/
|
|
8
|
+
const FILTER_KEY_REGEX = /^([^()]+)(\(([^)]+)\))?/;
|
|
9
|
+
/**
|
|
10
|
+
* Parses a filter key, returning its field name and function name (if defined) separately.
|
|
11
|
+
*/
|
|
12
|
+
export function parseFilterKey(key) {
|
|
13
|
+
const match = key.match(FILTER_KEY_REGEX);
|
|
14
|
+
const fieldNameWithFunction = match?.[3]?.trim();
|
|
15
|
+
const fieldName = fieldNameWithFunction || key.trim();
|
|
16
|
+
let functionName;
|
|
17
|
+
if (fieldNameWithFunction) {
|
|
18
|
+
functionName = match?.[1]?.trim();
|
|
19
|
+
return { fieldName, functionName };
|
|
20
|
+
}
|
|
21
|
+
return { fieldName, functionName };
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function parseNumericString(stringValue) {
|
|
2
|
+
let number = Number(stringValue);
|
|
3
|
+
if (isNaN(number) || !Number.isFinite(number)) {
|
|
4
|
+
return null; // invalid numbers
|
|
5
|
+
}
|
|
6
|
+
if (number > Number.MAX_SAFE_INTEGER || number < Number.MIN_SAFE_INTEGER) {
|
|
7
|
+
try {
|
|
8
|
+
number = BigInt(stringValue);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// BigInt parsing failed, e.g. it was a float larger than MAX_SAFE_INTEGER
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// casting parsed value back to string should be equal the original value
|
|
16
|
+
// (prevent unintended number parsing, e.g. String(7) !== "ob111")
|
|
17
|
+
if (String(number) !== stringValue) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return number;
|
|
21
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
+
import { InvalidQueryError } from '@directus/errors';
|
|
2
3
|
import { parseFilter, parseJSON } from '@directus/utils';
|
|
3
4
|
import { flatten, get, isPlainObject, merge, set } from 'lodash-es';
|
|
4
5
|
import { useLogger } from '../logger.js';
|
|
@@ -106,17 +107,21 @@ function sanitizeAggregate(rawAggregate) {
|
|
|
106
107
|
return aggregate;
|
|
107
108
|
}
|
|
108
109
|
function sanitizeFilter(rawFilter, accountability) {
|
|
109
|
-
const logger = useLogger();
|
|
110
110
|
let filters = rawFilter;
|
|
111
|
-
if (typeof
|
|
111
|
+
if (typeof filters === 'string') {
|
|
112
112
|
try {
|
|
113
|
-
filters = parseJSON(
|
|
113
|
+
filters = parseJSON(filters);
|
|
114
114
|
}
|
|
115
115
|
catch {
|
|
116
|
-
|
|
116
|
+
throw new InvalidQueryError({ reason: 'Invalid JSON for filter object' });
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
try {
|
|
120
|
+
return parseFilter(filters, accountability);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new InvalidQueryError({ reason: 'Invalid filter object' });
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
function sanitizeLimit(rawLimit) {
|
|
122
127
|
if (rawLimit === undefined || rawLimit === null)
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {} from 'knex';
|
|
2
|
+
import { getDatabaseClient } from '../database/index.js';
|
|
3
|
+
import { useLogger } from '../logger.js';
|
|
1
4
|
/**
|
|
2
5
|
* Execute the given handler within the current transaction or a newly created one
|
|
3
6
|
* if the current knex state isn't a transaction yet.
|
|
@@ -5,11 +8,45 @@
|
|
|
5
8
|
* Can be used to ensure the handler is run within a transaction,
|
|
6
9
|
* while preventing nested transactions.
|
|
7
10
|
*/
|
|
8
|
-
export const transaction = (knex, handler) => {
|
|
11
|
+
export const transaction = async (knex, handler) => {
|
|
9
12
|
if (knex.isTransaction) {
|
|
10
13
|
return handler(knex);
|
|
11
14
|
}
|
|
12
15
|
else {
|
|
13
|
-
|
|
16
|
+
try {
|
|
17
|
+
return await knex.transaction((trx) => handler(trx));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const client = getDatabaseClient(knex);
|
|
21
|
+
/**
|
|
22
|
+
* This error code indicates that the transaction failed due to another
|
|
23
|
+
* concurrent or recent transaction attempting to write to the same data.
|
|
24
|
+
* This can usually be solved by restarting the transaction on client-side
|
|
25
|
+
* after a short delay, so that it is executed against the latest state.
|
|
26
|
+
*
|
|
27
|
+
* @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
|
|
28
|
+
*/
|
|
29
|
+
const COCKROACH_RETRY_ERROR_CODE = '40001';
|
|
30
|
+
if (client !== 'cockroachdb' || error?.code !== COCKROACH_RETRY_ERROR_CODE)
|
|
31
|
+
throw error;
|
|
32
|
+
const MAX_ATTEMPTS = 3;
|
|
33
|
+
const BASE_DELAY = 100;
|
|
34
|
+
const logger = useLogger();
|
|
35
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
|
36
|
+
const delay = 2 ** attempt * BASE_DELAY;
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
38
|
+
logger.trace(`Restarting failed transaction (attempt ${attempt + 1}/${MAX_ATTEMPTS})`);
|
|
39
|
+
try {
|
|
40
|
+
return await knex.transaction((trx) => handler(trx));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error?.code !== COCKROACH_RETRY_ERROR_CODE)
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Initial execution + additional attempts */
|
|
48
|
+
const attempts = 1 + MAX_ATTEMPTS;
|
|
49
|
+
throw new Error(`Transaction failed after ${attempts} attempts`, { cause: error });
|
|
50
|
+
}
|
|
14
51
|
}
|
|
15
52
|
};
|
|
@@ -42,8 +42,6 @@ export function validateQuery(query) {
|
|
|
42
42
|
return query;
|
|
43
43
|
}
|
|
44
44
|
function validateFilter(filter) {
|
|
45
|
-
if (!filter)
|
|
46
|
-
throw new InvalidQueryError({ reason: 'Invalid filter object' });
|
|
47
45
|
for (const [key, nested] of Object.entries(filter)) {
|
|
48
46
|
if (key === '_and' || key === '_or') {
|
|
49
47
|
nested.forEach(validateFilter);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import getDatabase from '../database/index.js';
|
|
2
|
+
import { InvalidTokenError } from '@directus/errors';
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the associated session is still available and valid.
|
|
5
|
+
*
|
|
6
|
+
* @throws If session not found.
|
|
7
|
+
*/
|
|
8
|
+
export async function verifySessionJWT(payload) {
|
|
9
|
+
const database = getDatabase();
|
|
10
|
+
const session = await database
|
|
11
|
+
.select(1)
|
|
12
|
+
.from('directus_sessions')
|
|
13
|
+
.where({
|
|
14
|
+
token: payload['session'],
|
|
15
|
+
user: payload['id'],
|
|
16
|
+
})
|
|
17
|
+
.andWhere('expires', '>=', new Date())
|
|
18
|
+
.first();
|
|
19
|
+
if (!session) {
|
|
20
|
+
throw new InvalidTokenError();
|
|
21
|
+
}
|
|
22
|
+
}
|