@directus/api 25.0.1 → 26.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 +3 -3
- package/dist/auth/drivers/oauth2.d.ts +2 -0
- package/dist/auth/drivers/oauth2.js +40 -2
- package/dist/auth/drivers/openid.js +8 -1
- package/dist/controllers/access.js +2 -2
- package/dist/controllers/comments.js +2 -2
- package/dist/controllers/dashboards.js +2 -2
- package/dist/controllers/files.js +2 -2
- package/dist/controllers/flows.js +2 -2
- package/dist/controllers/folders.js +2 -2
- package/dist/controllers/items.js +2 -2
- package/dist/controllers/notifications.js +2 -2
- package/dist/controllers/operations.js +2 -2
- package/dist/controllers/panels.js +2 -2
- package/dist/controllers/permissions.js +2 -2
- package/dist/controllers/policies.js +2 -2
- package/dist/controllers/presets.js +2 -2
- package/dist/controllers/roles.js +2 -2
- package/dist/controllers/shares.js +2 -2
- package/dist/controllers/translations.js +2 -2
- package/dist/controllers/users.js +2 -2
- package/dist/controllers/utils.js +8 -3
- package/dist/controllers/versions.js +2 -2
- package/dist/controllers/webhooks.js +1 -1
- package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
- package/dist/database/helpers/capabilities/dialects/default.js +3 -0
- package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
- package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
- package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
- package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
- package/dist/database/helpers/capabilities/index.d.ts +7 -0
- package/dist/database/helpers/capabilities/index.js +7 -0
- package/dist/database/helpers/capabilities/types.d.ts +11 -0
- package/dist/database/helpers/capabilities/types.js +15 -0
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/postgres.js +0 -4
- package/dist/database/index.js +1 -1
- package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
- package/dist/database/migrations/20250224A-visual-editor.js +35 -0
- package/dist/database/run-ast/lib/get-db-query.js +16 -4
- package/dist/logger/index.js +3 -3
- package/dist/middleware/sanitize-query.js +17 -7
- package/dist/middleware/validate-batch.js +1 -1
- package/dist/operations/item-delete/index.js +1 -1
- package/dist/operations/item-read/index.js +1 -1
- package/dist/operations/item-update/index.js +1 -1
- package/dist/permissions/lib/fetch-permissions.js +6 -4
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
- package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
- package/dist/permissions/modules/process-payload/process-payload.js +13 -4
- package/dist/permissions/types.d.ts +2 -1
- package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
- package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
- package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
- package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
- package/dist/rate-limiter.js +1 -1
- package/dist/services/assets.js +12 -2
- package/dist/services/authentication.js +2 -2
- package/dist/services/collections.js +8 -2
- package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
- package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-field-type.js +51 -0
- package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/query.js +4 -4
- package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
- package/dist/services/graphql/resolvers/system-admin.js +207 -199
- package/dist/services/graphql/resolvers/system.d.ts +1 -7
- package/dist/services/graphql/resolvers/system.js +12 -113
- package/dist/services/graphql/schema/index.js +1 -1
- package/dist/services/graphql/schema/parse-query.d.ts +2 -2
- package/dist/services/graphql/schema/parse-query.js +6 -6
- package/dist/services/graphql/schema/read.d.ts +2 -2
- package/dist/services/graphql/schema/read.js +86 -2
- package/dist/services/graphql/schema-cache.d.ts +2 -2
- package/dist/services/graphql/schema-cache.js +1 -3
- package/dist/services/graphql/subscription.d.ts +3 -3
- package/dist/services/graphql/subscription.js +3 -3
- package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
- package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
- package/dist/services/items.d.ts +1 -0
- package/dist/services/items.js +30 -16
- package/dist/services/payload.d.ts +1 -0
- package/dist/services/payload.js +32 -17
- package/dist/services/shares.js +1 -1
- package/dist/services/specifications.js +10 -5
- package/dist/services/tus/lockers.d.ts +1 -1
- package/dist/services/tus/lockers.js +6 -5
- package/dist/services/tus/server.js +24 -0
- package/dist/services/users.js +1 -0
- package/dist/types/services.d.ts +2 -0
- package/dist/utils/apply-query.d.ts +1 -0
- package/dist/utils/apply-query.js +42 -31
- package/dist/utils/generate-hash.js +1 -1
- package/dist/utils/get-config-from-env.d.ts +6 -1
- package/dist/utils/get-config-from-env.js +16 -11
- package/dist/utils/get-graphql-type.js +3 -1
- package/dist/utils/is-login-redirect-allowed.js +2 -0
- package/dist/utils/redact-object.js +5 -1
- package/dist/utils/sanitize-query.d.ts +5 -2
- package/dist/utils/sanitize-query.js +34 -9
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/handlers/items.js +4 -4
- package/dist/websocket/handlers/subscribe.js +2 -2
- package/dist/websocket/messages.d.ts +7 -7
- package/dist/websocket/messages.js +1 -1
- package/package.json +60 -60
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
package/dist/services/payload.js
CHANGED
|
@@ -20,12 +20,14 @@ export class PayloadService {
|
|
|
20
20
|
helpers;
|
|
21
21
|
collection;
|
|
22
22
|
schema;
|
|
23
|
+
nested;
|
|
23
24
|
constructor(collection, options) {
|
|
24
25
|
this.accountability = options.accountability || null;
|
|
25
26
|
this.knex = options.knex || getDatabase();
|
|
26
27
|
this.helpers = getHelpers(this.knex);
|
|
27
28
|
this.collection = collection;
|
|
28
29
|
this.schema = options.schema;
|
|
30
|
+
this.nested = options.nested ?? [];
|
|
29
31
|
return this;
|
|
30
32
|
}
|
|
31
33
|
transformers = {
|
|
@@ -346,6 +348,7 @@ export class PayloadService {
|
|
|
346
348
|
accountability: this.accountability,
|
|
347
349
|
knex: this.knex,
|
|
348
350
|
schema: this.schema,
|
|
351
|
+
nested: [...this.nested, relation.field],
|
|
349
352
|
});
|
|
350
353
|
const relatedPrimaryKeyField = this.schema.collections[relatedCollection].primary;
|
|
351
354
|
const relatedRecord = payload[relation.field];
|
|
@@ -412,6 +415,7 @@ export class PayloadService {
|
|
|
412
415
|
accountability: this.accountability,
|
|
413
416
|
knex: this.knex,
|
|
414
417
|
schema: this.schema,
|
|
418
|
+
nested: [...this.nested, relation.field],
|
|
415
419
|
});
|
|
416
420
|
const relatedRecord = payload[relation.field];
|
|
417
421
|
if (['string', 'number'].includes(typeof relatedRecord))
|
|
@@ -482,6 +486,7 @@ export class PayloadService {
|
|
|
482
486
|
accountability: this.accountability,
|
|
483
487
|
knex: this.knex,
|
|
484
488
|
schema: this.schema,
|
|
489
|
+
nested: [...this.nested, relation.meta.one_field],
|
|
485
490
|
});
|
|
486
491
|
const recordsToUpsert = [];
|
|
487
492
|
const savedPrimaryKeys = [];
|
|
@@ -490,15 +495,23 @@ export class PayloadService {
|
|
|
490
495
|
if (!field || Array.isArray(field)) {
|
|
491
496
|
const updates = field || []; // treat falsey values as removing all children
|
|
492
497
|
for (let i = 0; i < updates.length; i++) {
|
|
498
|
+
const currentId = parent || payload[currentPrimaryKeyField];
|
|
493
499
|
const relatedRecord = updates[i];
|
|
500
|
+
const relatedId = typeof relatedRecord === 'string' || typeof relatedRecord === 'number'
|
|
501
|
+
? relatedRecord
|
|
502
|
+
: relatedRecord[relatedPrimaryKeyField];
|
|
494
503
|
let record = cloneDeep(relatedRecord);
|
|
495
|
-
|
|
496
|
-
|
|
504
|
+
let existingRecord;
|
|
505
|
+
// No relatedId means it's a new record
|
|
506
|
+
if (relatedId) {
|
|
507
|
+
existingRecord = await this.knex
|
|
497
508
|
.select(relatedPrimaryKeyField, relation.field)
|
|
498
509
|
.from(relation.collection)
|
|
499
|
-
.where({ [relatedPrimaryKeyField]:
|
|
510
|
+
.where({ [relatedPrimaryKeyField]: relatedId })
|
|
500
511
|
.first();
|
|
501
|
-
|
|
512
|
+
}
|
|
513
|
+
if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
|
|
514
|
+
if (!existingRecord) {
|
|
502
515
|
throw new ForbiddenError();
|
|
503
516
|
}
|
|
504
517
|
// If the related item is already associated to the current item, and there's no
|
|
@@ -507,9 +520,7 @@ export class PayloadService {
|
|
|
507
520
|
// for items that aren't actually being updated. NOTE: We use == here, as the
|
|
508
521
|
// primary key might be reported as a string instead of number, coming from the
|
|
509
522
|
// http route, and or a bigInteger in the DB
|
|
510
|
-
if (isNil(existingRecord[relation.field]) === false &&
|
|
511
|
-
(existingRecord[relation.field] == parent ||
|
|
512
|
-
existingRecord[relation.field] == payload[currentPrimaryKeyField])) {
|
|
523
|
+
if (isNil(existingRecord[relation.field]) === false && existingRecord[relation.field] == currentId) {
|
|
513
524
|
savedPrimaryKeys.push(existingRecord[relatedPrimaryKeyField]);
|
|
514
525
|
continue;
|
|
515
526
|
}
|
|
@@ -517,10 +528,10 @@ export class PayloadService {
|
|
|
517
528
|
[relatedPrimaryKeyField]: relatedRecord,
|
|
518
529
|
};
|
|
519
530
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
531
|
+
if (!existingRecord || existingRecord[relation.field] != parent) {
|
|
532
|
+
record[relation.field] = currentId;
|
|
533
|
+
}
|
|
534
|
+
recordsToUpsert.push(record);
|
|
524
535
|
}
|
|
525
536
|
savedPrimaryKeys.push(...(await service.upsertMany(recordsToUpsert, {
|
|
526
537
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
@@ -608,13 +619,17 @@ export class PayloadService {
|
|
|
608
619
|
});
|
|
609
620
|
}
|
|
610
621
|
if (alterations.update) {
|
|
611
|
-
const primaryKeyField = this.schema.collections[relation.collection].primary;
|
|
612
622
|
for (const item of alterations.update) {
|
|
613
|
-
const { [
|
|
614
|
-
await
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
623
|
+
const { [relatedPrimaryKeyField]: key, ...record } = item;
|
|
624
|
+
const existingRecord = await this.knex
|
|
625
|
+
.select(relatedPrimaryKeyField, relation.field)
|
|
626
|
+
.from(relation.collection)
|
|
627
|
+
.where({ [relatedPrimaryKeyField]: key })
|
|
628
|
+
.first();
|
|
629
|
+
if (!existingRecord || existingRecord[relation.field] != parent) {
|
|
630
|
+
record[relation.field] = parent || payload[currentPrimaryKeyField];
|
|
631
|
+
}
|
|
632
|
+
await service.updateOne(key, record, {
|
|
618
633
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
619
634
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
620
635
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
package/dist/services/shares.js
CHANGED
|
@@ -4,6 +4,7 @@ import argon2 from 'argon2';
|
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
import { useLogger } from '../logger/index.js';
|
|
7
|
+
import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
|
|
7
8
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
8
9
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
9
10
|
import { getSecret } from '../utils/get-secret.js';
|
|
@@ -13,7 +14,6 @@ import { userName } from '../utils/user-name.js';
|
|
|
13
14
|
import { ItemsService } from './items.js';
|
|
14
15
|
import { MailService } from './mail/index.js';
|
|
15
16
|
import { UsersService } from './users.js';
|
|
16
|
-
import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
|
|
17
17
|
const env = useEnv();
|
|
18
18
|
const logger = useLogger();
|
|
19
19
|
export class SharesService extends ItemsService {
|
|
@@ -49,7 +49,7 @@ class OASSpecsService {
|
|
|
49
49
|
permissions = await fetchPermissions({ policies, accountability: this.accountability }, { schema: this.schema, knex: this.knex });
|
|
50
50
|
}
|
|
51
51
|
const tags = await this.generateTags(schemaForSpec);
|
|
52
|
-
const paths = await this.generatePaths(permissions, tags);
|
|
52
|
+
const paths = await this.generatePaths(schemaForSpec, permissions, tags);
|
|
53
53
|
const components = await this.generateComponents(schemaForSpec, tags);
|
|
54
54
|
const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
|
|
55
55
|
const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
|
|
@@ -114,7 +114,7 @@ class OASSpecsService {
|
|
|
114
114
|
// Filter out the generic Items information
|
|
115
115
|
return tags.filter((tag) => tag.name !== 'Items');
|
|
116
116
|
}
|
|
117
|
-
async generatePaths(permissions, tags) {
|
|
117
|
+
async generatePaths(schema, permissions, tags) {
|
|
118
118
|
const paths = {};
|
|
119
119
|
if (!tags)
|
|
120
120
|
return paths;
|
|
@@ -195,11 +195,16 @@ class OASSpecsService {
|
|
|
195
195
|
'application/json': {
|
|
196
196
|
schema: {
|
|
197
197
|
properties: {
|
|
198
|
-
data:
|
|
199
|
-
|
|
198
|
+
data: schema.collections[collection]?.singleton
|
|
199
|
+
? {
|
|
200
200
|
$ref: `#/components/schemas/${tag.name}`,
|
|
201
|
+
}
|
|
202
|
+
: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
items: {
|
|
205
|
+
$ref: `#/components/schemas/${tag.name}`,
|
|
206
|
+
},
|
|
201
207
|
},
|
|
202
|
-
},
|
|
203
208
|
},
|
|
204
209
|
},
|
|
205
210
|
},
|
|
@@ -29,7 +29,7 @@ export declare class KvLock implements Lock {
|
|
|
29
29
|
private acquireTimeout;
|
|
30
30
|
private kv;
|
|
31
31
|
constructor(id: string, lockTimeout?: number, acquireTimeout?: number);
|
|
32
|
-
lock(cancelReq: RequestRelease): Promise<void>;
|
|
32
|
+
lock(signal: AbortSignal, cancelReq: RequestRelease): Promise<void>;
|
|
33
33
|
protected acquireLock(id: string, requestRelease: RequestRelease, signal: AbortSignal): Promise<boolean>;
|
|
34
34
|
unlock(): Promise<void>;
|
|
35
35
|
}
|
|
@@ -38,11 +38,12 @@ export class KvLock {
|
|
|
38
38
|
this.acquireTimeout = acquireTimeout;
|
|
39
39
|
this.kv = useLock();
|
|
40
40
|
}
|
|
41
|
-
async lock(cancelReq) {
|
|
41
|
+
async lock(signal, cancelReq) {
|
|
42
42
|
const abortController = new AbortController();
|
|
43
|
+
const abortSignal = AbortSignal.any([signal, abortController.signal]);
|
|
43
44
|
const lock = await Promise.race([
|
|
44
|
-
waitTimeout(this.acquireTimeout,
|
|
45
|
-
this.acquireLock(this.id, cancelReq,
|
|
45
|
+
waitTimeout(this.acquireTimeout, abortSignal),
|
|
46
|
+
this.acquireLock(this.id, cancelReq, abortSignal),
|
|
46
47
|
]);
|
|
47
48
|
abortController.abort();
|
|
48
49
|
if (!lock) {
|
|
@@ -50,10 +51,10 @@ export class KvLock {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
async acquireLock(id, requestRelease, signal) {
|
|
54
|
+
const lockTime = await this.kv.get(id);
|
|
53
55
|
if (signal.aborted) {
|
|
54
|
-
return
|
|
56
|
+
return typeof lockTime !== 'undefined';
|
|
55
57
|
}
|
|
56
|
-
const lockTime = await this.kv.get(id);
|
|
57
58
|
const now = Date.now();
|
|
58
59
|
if (!lockTime || Number(lockTime) < now - this.lockTimeout) {
|
|
59
60
|
await this.kv.set(id, now);
|
|
@@ -14,6 +14,8 @@ import { ItemsService } from '../index.js';
|
|
|
14
14
|
import { TusDataStore } from './data-store.js';
|
|
15
15
|
import { getTusLocker } from './lockers.js';
|
|
16
16
|
import { pick } from 'lodash-es';
|
|
17
|
+
import emitter from '../../emitter.js';
|
|
18
|
+
import getDatabase from '../../database/index.js';
|
|
17
19
|
async function createTusStore(context) {
|
|
18
20
|
const env = useEnv();
|
|
19
21
|
const storage = await getStorage();
|
|
@@ -48,6 +50,7 @@ export async function createTusServer(context) {
|
|
|
48
50
|
}))[0];
|
|
49
51
|
if (!file)
|
|
50
52
|
return res;
|
|
53
|
+
let fileData;
|
|
51
54
|
// update metadata when file is replaced
|
|
52
55
|
if (file.tus_data?.['metadata']?.['replace_id']) {
|
|
53
56
|
const newFile = await service.readOne(file.tus_data['metadata']['replace_id']);
|
|
@@ -60,6 +63,12 @@ export async function createTusServer(context) {
|
|
|
60
63
|
...updateFields,
|
|
61
64
|
...metadata,
|
|
62
65
|
});
|
|
66
|
+
fileData = {
|
|
67
|
+
...newFile,
|
|
68
|
+
...updateFields,
|
|
69
|
+
...metadata,
|
|
70
|
+
id: file.tus_data['metadata']['replace_id'],
|
|
71
|
+
};
|
|
63
72
|
await service.deleteOne(file.id);
|
|
64
73
|
}
|
|
65
74
|
else {
|
|
@@ -69,7 +78,22 @@ export async function createTusServer(context) {
|
|
|
69
78
|
tus_id: null,
|
|
70
79
|
tus_data: null,
|
|
71
80
|
});
|
|
81
|
+
fileData = {
|
|
82
|
+
...file,
|
|
83
|
+
...metadata,
|
|
84
|
+
tus_id: null,
|
|
85
|
+
tus_data: null,
|
|
86
|
+
};
|
|
72
87
|
}
|
|
88
|
+
emitter.emitAction('files.upload', {
|
|
89
|
+
payload: fileData,
|
|
90
|
+
key: fileData.id,
|
|
91
|
+
collection: 'directus_files',
|
|
92
|
+
}, {
|
|
93
|
+
database: getDatabase(),
|
|
94
|
+
schema: req.schema,
|
|
95
|
+
accountability: req.accountability,
|
|
96
|
+
});
|
|
73
97
|
return res;
|
|
74
98
|
},
|
|
75
99
|
generateUrl(_req, opts) {
|
package/dist/services/users.js
CHANGED
package/dist/types/services.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export type AbstractServiceOptions = {
|
|
|
4
4
|
knex?: Knex | undefined;
|
|
5
5
|
accountability?: Accountability | null | undefined;
|
|
6
6
|
schema: SchemaOverview;
|
|
7
|
+
nested?: string[];
|
|
7
8
|
};
|
|
8
9
|
export interface AbstractService {
|
|
9
10
|
knex: Knex;
|
|
10
11
|
accountability: Accountability | null | undefined;
|
|
12
|
+
nested: string[];
|
|
11
13
|
createOne(data: Partial<Item>): Promise<PrimaryKey>;
|
|
12
14
|
createMany(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
|
13
15
|
readOne(key: PrimaryKey, query?: Query): Promise<Item>;
|
|
@@ -47,20 +47,28 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
|
|
|
47
47
|
hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
|
|
48
48
|
}
|
|
49
49
|
if (query.group) {
|
|
50
|
+
const helpers = getHelpers(knex);
|
|
50
51
|
const rawColumns = query.group.map((column) => getColumn(knex, collection, column, false, schema));
|
|
51
52
|
let columns;
|
|
52
53
|
if (options?.groupWhenCases) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
column,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if (helpers.capabilities.supportsColumnPositionInGroupBy() && options.groupColumnPositions) {
|
|
55
|
+
// This can be streamlined for databases that support reusing the alias in group by expressions
|
|
56
|
+
columns = query.group.map((column, index) => options.groupColumnPositions[index] !== undefined ? knex.raw(options.groupColumnPositions[index]) : column);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Reconstruct the columns with the case/when logic
|
|
60
|
+
columns = rawColumns.map((column, index) => applyCaseWhen({
|
|
61
|
+
columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
|
|
62
|
+
column,
|
|
63
|
+
aliasMap,
|
|
64
|
+
cases,
|
|
65
|
+
table: collection,
|
|
66
|
+
permissions,
|
|
67
|
+
}, {
|
|
68
|
+
knex,
|
|
69
|
+
schema,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
64
72
|
if (query.sort && query.sort.length === 1 && query.sort[0] === query.group[0]) {
|
|
65
73
|
// Special case, where the sort query is injected by the group by operation
|
|
66
74
|
dbQuery.clear('order');
|
|
@@ -352,26 +360,29 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
352
360
|
pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
|
|
353
361
|
}
|
|
354
362
|
const childKey = Object.keys(value)?.[0];
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
363
|
+
const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
|
|
364
|
+
const field = relation.field;
|
|
365
|
+
const collection = relation.collection;
|
|
366
|
+
const column = `${collection}.${field}`;
|
|
367
|
+
subQueryKnex
|
|
368
|
+
.select({ [field]: column })
|
|
369
|
+
.from(collection)
|
|
370
|
+
.whereNotNull(column);
|
|
371
|
+
applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
|
|
372
|
+
};
|
|
373
|
+
const { cases: subCases } = getCases(relation.collection, permissions, []);
|
|
374
|
+
if (childKey === '_none') {
|
|
375
|
+
dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
else if (childKey === '_some') {
|
|
379
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Add implicit _some behavior when no operator is provided
|
|
384
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(value, subCases));
|
|
385
|
+
continue;
|
|
375
386
|
}
|
|
376
387
|
}
|
|
377
388
|
if (filterPath.includes('_none') || filterPath.includes('_some')) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import argon2 from 'argon2';
|
|
2
2
|
import { getConfigFromEnv } from './get-config-from-env.js';
|
|
3
3
|
export function generateHash(stringToHash) {
|
|
4
|
-
const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
|
4
|
+
const argon2HashConfigOptions = getConfigFromEnv('HASH_', { omitPrefix: 'HASH_RAW' }); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
|
5
5
|
// associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata
|
|
6
6
|
if ('associatedData' in argon2HashConfigOptions)
|
|
7
7
|
argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']);
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface GetConfigFromEnvOptions {
|
|
2
|
+
omitPrefix?: string | string[];
|
|
3
|
+
omitKey?: string | string[];
|
|
4
|
+
type?: 'camelcase' | 'underscore';
|
|
5
|
+
}
|
|
6
|
+
export declare function getConfigFromEnv(prefix: string, options?: GetConfigFromEnvOptions): Record<string, any>;
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toArray } from '@directus/utils';
|
|
2
3
|
import camelcase from 'camelcase';
|
|
3
4
|
import { set } from 'lodash-es';
|
|
4
|
-
export function getConfigFromEnv(prefix,
|
|
5
|
+
export function getConfigFromEnv(prefix, options) {
|
|
5
6
|
const env = useEnv();
|
|
7
|
+
const type = options?.type ?? 'camelcase';
|
|
6
8
|
const config = {};
|
|
9
|
+
const lowerCasePrefix = prefix.toLowerCase();
|
|
10
|
+
const omitKeys = toArray(options?.omitKey ?? []).map((key) => key.toLowerCase());
|
|
11
|
+
const omitPrefixes = toArray(options?.omitPrefix ?? []).map((prefix) => prefix.toLowerCase());
|
|
7
12
|
for (const [key, value] of Object.entries(env)) {
|
|
8
|
-
|
|
13
|
+
const lowerCaseKey = key.toLowerCase();
|
|
14
|
+
if (lowerCaseKey.startsWith(lowerCasePrefix) === false)
|
|
9
15
|
continue;
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (matches)
|
|
16
|
+
if (omitKeys.length > 0) {
|
|
17
|
+
const isKeyInOmitKeys = omitKeys.some((keyToOmit) => lowerCaseKey === keyToOmit);
|
|
18
|
+
if (isKeyInOmitKeys)
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (omitPrefixes.length > 0) {
|
|
22
|
+
const keyStartsWithAnyPrefix = omitPrefixes.some((prefix) => lowerCaseKey.startsWith(prefix));
|
|
23
|
+
if (keyStartsWithAnyPrefix)
|
|
19
24
|
continue;
|
|
20
25
|
}
|
|
21
26
|
if (key.includes('__')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLScalarType, GraphQLString } from 'graphql';
|
|
1
|
+
import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLScalarType, GraphQLString, } from 'graphql';
|
|
2
2
|
import { GraphQLJSON } from 'graphql-compose';
|
|
3
3
|
import { GraphQLBigInt } from '../services/graphql/types/bigint.js';
|
|
4
4
|
import { GraphQLDate } from '../services/graphql/types/date.js';
|
|
@@ -31,6 +31,8 @@ export function getGraphQLType(localType, special) {
|
|
|
31
31
|
return GraphQLDate;
|
|
32
32
|
case 'hash':
|
|
33
33
|
return GraphQLHash;
|
|
34
|
+
case 'uuid':
|
|
35
|
+
return GraphQLID;
|
|
34
36
|
default:
|
|
35
37
|
return GraphQLString;
|
|
36
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { toArray } from '@directus/utils';
|
|
3
|
+
import { useLogger } from '../logger/index.js';
|
|
3
4
|
import isUrlAllowed from './is-url-allowed.js';
|
|
4
5
|
/**
|
|
5
6
|
* Checks if the defined redirect after successful SSO login is in the allow list
|
|
@@ -26,6 +27,7 @@ export function isLoginRedirectAllowed(redirect, provider) {
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
if (URL.canParse(publicUrl) === false) {
|
|
30
|
+
useLogger().error('Invalid PUBLIC_URL for login redirect');
|
|
29
31
|
return false;
|
|
30
32
|
}
|
|
31
33
|
// allow redirects to the defined PUBLIC_URL
|
|
@@ -116,7 +116,8 @@ export function getReplacer(replacement, values) {
|
|
|
116
116
|
let finalValue = value;
|
|
117
117
|
for (const [redactKey, valueToRedact] of filteredValues) {
|
|
118
118
|
if (finalValue.includes(valueToRedact)) {
|
|
119
|
-
|
|
119
|
+
const regexp = new RegExp(escapeRegexString(valueToRedact), 'g');
|
|
120
|
+
finalValue = finalValue.replace(regexp, replacement(redactKey));
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return finalValue;
|
|
@@ -125,3 +126,6 @@ export function getReplacer(replacement, values) {
|
|
|
125
126
|
const seen = new WeakSet();
|
|
126
127
|
return replacer(seen);
|
|
127
128
|
}
|
|
129
|
+
function escapeRegexString(string) {
|
|
130
|
+
return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
131
|
+
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import type { Accountability, Query } from '@directus/types';
|
|
2
|
-
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize the query parameters and parse them where necessary.
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeQuery(rawQuery: Record<string, any>, schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
|
|
@@ -2,9 +2,17 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { InvalidQueryError } from '@directus/errors';
|
|
3
3
|
import { parseFilter, parseJSON } from '@directus/utils';
|
|
4
4
|
import { flatten, get, isPlainObject, merge, set } from 'lodash-es';
|
|
5
|
+
import getDatabase from '../database/index.js';
|
|
5
6
|
import { useLogger } from '../logger/index.js';
|
|
7
|
+
import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
|
|
8
|
+
import { contextHasDynamicVariables } from '../permissions/modules/process-ast/utils/context-has-dynamic-variables.js';
|
|
9
|
+
import { extractRequiredDynamicVariableContext } from '../permissions/utils/extract-required-dynamic-variable-context.js';
|
|
10
|
+
import { fetchDynamicVariableData } from '../permissions/utils/fetch-dynamic-variable-data.js';
|
|
6
11
|
import { Meta } from '../types/index.js';
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize the query parameters and parse them where necessary.
|
|
14
|
+
*/
|
|
15
|
+
export async function sanitizeQuery(rawQuery, schema, accountability) {
|
|
8
16
|
const env = useEnv();
|
|
9
17
|
const query = {};
|
|
10
18
|
const hasMaxLimit = 'QUERY_LIMIT_MAX' in env &&
|
|
@@ -33,7 +41,7 @@ export function sanitizeQuery(rawQuery, accountability) {
|
|
|
33
41
|
query.sort = sanitizeSort(rawQuery['sort']);
|
|
34
42
|
}
|
|
35
43
|
if (rawQuery['filter']) {
|
|
36
|
-
query.filter = sanitizeFilter(rawQuery['filter'], accountability || null);
|
|
44
|
+
query.filter = await sanitizeFilter(rawQuery['filter'], schema, accountability || null);
|
|
37
45
|
}
|
|
38
46
|
if (rawQuery['offset'] !== undefined) {
|
|
39
47
|
query.offset = sanitizeOffset(rawQuery['offset']);
|
|
@@ -58,7 +66,7 @@ export function sanitizeQuery(rawQuery, accountability) {
|
|
|
58
66
|
if (rawQuery['deep']) {
|
|
59
67
|
if (!query.deep)
|
|
60
68
|
query.deep = {};
|
|
61
|
-
query.deep = sanitizeDeep(rawQuery['deep'], accountability);
|
|
69
|
+
query.deep = await sanitizeDeep(rawQuery['deep'], schema, accountability);
|
|
62
70
|
}
|
|
63
71
|
if (rawQuery['alias']) {
|
|
64
72
|
query.alias = sanitizeAlias(rawQuery['alias']);
|
|
@@ -106,7 +114,7 @@ function sanitizeAggregate(rawAggregate) {
|
|
|
106
114
|
}
|
|
107
115
|
return aggregate;
|
|
108
116
|
}
|
|
109
|
-
function sanitizeFilter(rawFilter, accountability) {
|
|
117
|
+
async function sanitizeFilter(rawFilter, schema, accountability) {
|
|
110
118
|
let filters = rawFilter;
|
|
111
119
|
if (typeof filters === 'string') {
|
|
112
120
|
try {
|
|
@@ -117,7 +125,24 @@ function sanitizeFilter(rawFilter, accountability) {
|
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
try {
|
|
120
|
-
|
|
128
|
+
let filterContext;
|
|
129
|
+
if (accountability) {
|
|
130
|
+
const dynamicVariableContext = extractRequiredDynamicVariableContext(filters);
|
|
131
|
+
if (contextHasDynamicVariables(dynamicVariableContext)) {
|
|
132
|
+
const context = {
|
|
133
|
+
schema,
|
|
134
|
+
knex: getDatabase(),
|
|
135
|
+
};
|
|
136
|
+
const policies = await fetchPolicies(accountability, context);
|
|
137
|
+
context.accountability = accountability;
|
|
138
|
+
filterContext = await fetchDynamicVariableData({
|
|
139
|
+
dynamicVariableContext,
|
|
140
|
+
accountability,
|
|
141
|
+
policies,
|
|
142
|
+
}, context);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return parseFilter(filters, accountability, filterContext);
|
|
121
146
|
}
|
|
122
147
|
catch {
|
|
123
148
|
throw new InvalidQueryError({ reason: 'Invalid filter object' });
|
|
@@ -146,7 +171,7 @@ function sanitizeMeta(rawMeta) {
|
|
|
146
171
|
}
|
|
147
172
|
return [rawMeta];
|
|
148
173
|
}
|
|
149
|
-
function sanitizeDeep(deep, accountability) {
|
|
174
|
+
async function sanitizeDeep(deep, schema, accountability) {
|
|
150
175
|
const logger = useLogger();
|
|
151
176
|
const result = {};
|
|
152
177
|
if (typeof deep === 'string') {
|
|
@@ -157,9 +182,9 @@ function sanitizeDeep(deep, accountability) {
|
|
|
157
182
|
logger.warn('Invalid value passed for deep query parameter.');
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
|
-
parse(deep);
|
|
185
|
+
await parse(deep);
|
|
161
186
|
return result;
|
|
162
|
-
function parse(level, path = []) {
|
|
187
|
+
async function parse(level, path = []) {
|
|
163
188
|
const subQuery = {};
|
|
164
189
|
const parsedLevel = {};
|
|
165
190
|
for (const [key, value] of Object.entries(level)) {
|
|
@@ -175,7 +200,7 @@ function sanitizeDeep(deep, accountability) {
|
|
|
175
200
|
}
|
|
176
201
|
if (Object.keys(subQuery).length > 0) {
|
|
177
202
|
// Sanitize the entire sub query
|
|
178
|
-
const parsedSubQuery = sanitizeQuery(subQuery, accountability);
|
|
203
|
+
const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability);
|
|
179
204
|
for (const [parsedKey, parsedValue] of Object.entries(parsedSubQuery)) {
|
|
180
205
|
parsedLevel[`_${parsedKey}`] = parsedValue;
|
|
181
206
|
}
|
|
@@ -2,11 +2,11 @@ import type { Accountability } from '@directus/types';
|
|
|
2
2
|
import type { IncomingMessage, Server as httpServer } from 'http';
|
|
3
3
|
import type { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
4
4
|
import type internal from 'stream';
|
|
5
|
-
import WebSocket from 'ws';
|
|
5
|
+
import WebSocket, { type Server } from 'ws';
|
|
6
6
|
import { WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
|
|
7
7
|
import type { AuthenticationState, UpgradeContext, WebSocketAuthentication, WebSocketClient } from '../types.js';
|
|
8
8
|
export default abstract class SocketController {
|
|
9
|
-
server:
|
|
9
|
+
server: Server;
|
|
10
10
|
clients: Set<WebSocketClient>;
|
|
11
11
|
authentication: WebSocketAuthentication;
|
|
12
12
|
endpoint: string;
|