@directus/api 25.0.0 → 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 +39 -3
- package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
- package/dist/services/fields/build-collection-and-field-relations.js +55 -0
- package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
- package/dist/services/fields/get-collection-meta-updates.js +72 -0
- package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
- package/dist/services/fields/get-collection-relation-list.js +28 -0
- package/dist/services/fields.js +17 -12
- 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/meta.js +4 -2
- 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 +58 -58
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Accountability, Query } from '@directus/types';
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { GraphQLResolveInfo, SelectionNode } from 'graphql';
|
|
3
3
|
/**
|
|
4
4
|
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
5
5
|
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
6
6
|
*/
|
|
7
|
-
export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], accountability?: Accountability | null): Query
|
|
7
|
+
export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
|
|
@@ -7,8 +7,8 @@ import { parseArgs } from './parse-args.js';
|
|
|
7
7
|
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
8
8
|
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
9
9
|
*/
|
|
10
|
-
export function getQuery(rawQuery, selections, variableValues, accountability) {
|
|
11
|
-
const query = sanitizeQuery(rawQuery, accountability);
|
|
10
|
+
export async function getQuery(rawQuery, selections, variableValues, schema, accountability) {
|
|
11
|
+
const query = await sanitizeQuery(rawQuery, schema, accountability);
|
|
12
12
|
const parseAliases = (selections) => {
|
|
13
13
|
const aliases = {};
|
|
14
14
|
for (const selection of selections) {
|
|
@@ -20,7 +20,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
|
|
|
20
20
|
}
|
|
21
21
|
return aliases;
|
|
22
22
|
};
|
|
23
|
-
const parseFields = (selections, parent) => {
|
|
23
|
+
const parseFields = async (selections, parent) => {
|
|
24
24
|
const fields = [];
|
|
25
25
|
for (let selection of selections) {
|
|
26
26
|
if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true)
|
|
@@ -70,7 +70,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
|
|
73
|
+
children = await parseFields(selection.selectionSet.selections, currentAlias ?? current);
|
|
74
74
|
}
|
|
75
75
|
fields.push(...children);
|
|
76
76
|
}
|
|
@@ -82,14 +82,14 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
|
|
|
82
82
|
if (!query.deep)
|
|
83
83
|
query.deep = {};
|
|
84
84
|
const args = parseArgs(selection.arguments, variableValues);
|
|
85
|
-
set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(sanitizeQuery(args, accountability), (_value, key) => `_${key}`)));
|
|
85
|
+
set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
return uniq(fields);
|
|
90
90
|
};
|
|
91
91
|
query.alias = parseAliases(selections);
|
|
92
|
-
query.fields = parseFields(selections);
|
|
92
|
+
query.fields = await parseFields(selections);
|
|
93
93
|
if (query.filter)
|
|
94
94
|
query.filter = replaceFuncs(query.filter);
|
|
95
95
|
query.deep = replaceFuncs(query.deep);
|
|
@@ -5,8 +5,8 @@ import { type InconsistentFields, type Schema } from './index.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Create readable types and attach resolvers for each. Also prepares full filter argument structures
|
|
7
7
|
*/
|
|
8
|
-
export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): {
|
|
8
|
+
export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): Promise<{
|
|
9
9
|
ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
10
10
|
VersionCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
11
11
|
ReadableCollectionFilterTypes: Record<string, InputTypeComposer<any>>;
|
|
12
|
-
}
|
|
12
|
+
}>;
|
|
@@ -14,12 +14,75 @@ import { getTypes } from './get-types.js';
|
|
|
14
14
|
/**
|
|
15
15
|
* Create readable types and attach resolvers for each. Also prepares full filter argument structures
|
|
16
16
|
*/
|
|
17
|
-
export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
|
|
17
|
+
export async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
|
|
18
18
|
const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'read');
|
|
19
19
|
const ReadableCollectionFilterTypes = {};
|
|
20
|
+
const ReadableCollectionQuantifierFilterTypes = {};
|
|
20
21
|
const AggregatedFunctions = {};
|
|
21
22
|
const AggregatedFields = {};
|
|
22
23
|
const AggregateMethods = {};
|
|
24
|
+
const IDFilterOperators = schemaComposer.createInputTC({
|
|
25
|
+
name: 'id_filter_operators',
|
|
26
|
+
fields: {
|
|
27
|
+
_eq: {
|
|
28
|
+
type: GraphQLID,
|
|
29
|
+
},
|
|
30
|
+
_neq: {
|
|
31
|
+
type: GraphQLID,
|
|
32
|
+
},
|
|
33
|
+
_contains: {
|
|
34
|
+
type: GraphQLID,
|
|
35
|
+
},
|
|
36
|
+
_icontains: {
|
|
37
|
+
type: GraphQLID,
|
|
38
|
+
},
|
|
39
|
+
_ncontains: {
|
|
40
|
+
type: GraphQLID,
|
|
41
|
+
},
|
|
42
|
+
_starts_with: {
|
|
43
|
+
type: GraphQLID,
|
|
44
|
+
},
|
|
45
|
+
_nstarts_with: {
|
|
46
|
+
type: GraphQLID,
|
|
47
|
+
},
|
|
48
|
+
_istarts_with: {
|
|
49
|
+
type: GraphQLID,
|
|
50
|
+
},
|
|
51
|
+
_nistarts_with: {
|
|
52
|
+
type: GraphQLID,
|
|
53
|
+
},
|
|
54
|
+
_ends_with: {
|
|
55
|
+
type: GraphQLID,
|
|
56
|
+
},
|
|
57
|
+
_nends_with: {
|
|
58
|
+
type: GraphQLID,
|
|
59
|
+
},
|
|
60
|
+
_iends_with: {
|
|
61
|
+
type: GraphQLID,
|
|
62
|
+
},
|
|
63
|
+
_niends_with: {
|
|
64
|
+
type: GraphQLID,
|
|
65
|
+
},
|
|
66
|
+
_in: {
|
|
67
|
+
type: new GraphQLList(GraphQLID),
|
|
68
|
+
},
|
|
69
|
+
_nin: {
|
|
70
|
+
type: new GraphQLList(GraphQLID),
|
|
71
|
+
},
|
|
72
|
+
_null: {
|
|
73
|
+
type: GraphQLBoolean,
|
|
74
|
+
},
|
|
75
|
+
_nnull: {
|
|
76
|
+
type: GraphQLBoolean,
|
|
77
|
+
},
|
|
78
|
+
_empty: {
|
|
79
|
+
type: GraphQLBoolean,
|
|
80
|
+
},
|
|
81
|
+
_nempty: {
|
|
82
|
+
type: GraphQLBoolean,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
23
86
|
const StringFilterOperators = schemaComposer.createInputTC({
|
|
24
87
|
name: 'string_filter_operators',
|
|
25
88
|
fields: {
|
|
@@ -356,6 +419,9 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
|
|
|
356
419
|
case GraphQLHash:
|
|
357
420
|
filterOperatorType = HashFilterOperators;
|
|
358
421
|
break;
|
|
422
|
+
case GraphQLID:
|
|
423
|
+
filterOperatorType = IDFilterOperators;
|
|
424
|
+
break;
|
|
359
425
|
default:
|
|
360
426
|
filterOperatorType = StringFilterOperators;
|
|
361
427
|
}
|
|
@@ -591,10 +657,21 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
|
|
|
591
657
|
});
|
|
592
658
|
}
|
|
593
659
|
}
|
|
660
|
+
for (const collection in ReadableCollectionFilterTypes) {
|
|
661
|
+
const quantifier_collection = ReadableCollectionFilterTypes[collection]?.clone(`${collection}_quantifier_filter`);
|
|
662
|
+
quantifier_collection?.addFields({
|
|
663
|
+
_some: ReadableCollectionFilterTypes[collection],
|
|
664
|
+
_none: ReadableCollectionFilterTypes[collection],
|
|
665
|
+
});
|
|
666
|
+
ReadableCollectionQuantifierFilterTypes[collection] = quantifier_collection;
|
|
667
|
+
}
|
|
594
668
|
for (const relation of schema.read.relations) {
|
|
595
669
|
if (relation.related_collection) {
|
|
596
670
|
if (SYSTEM_DENY_LIST.includes(relation.related_collection))
|
|
597
671
|
continue;
|
|
672
|
+
ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
|
|
673
|
+
[relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
|
|
674
|
+
});
|
|
598
675
|
ReadableCollectionFilterTypes[relation.collection]?.addFields({
|
|
599
676
|
[relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
|
|
600
677
|
});
|
|
@@ -617,8 +694,11 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
|
|
|
617
694
|
},
|
|
618
695
|
});
|
|
619
696
|
if (relation.meta?.one_field) {
|
|
697
|
+
ReadableCollectionQuantifierFilterTypes[relation.related_collection]?.addFields({
|
|
698
|
+
[relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
|
|
699
|
+
});
|
|
620
700
|
ReadableCollectionFilterTypes[relation.related_collection]?.addFields({
|
|
621
|
-
[relation.meta.one_field]:
|
|
701
|
+
[relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
|
|
622
702
|
});
|
|
623
703
|
ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, {
|
|
624
704
|
filter: ReadableCollectionFilterTypes[relation.collection],
|
|
@@ -641,8 +721,12 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
|
|
|
641
721
|
}
|
|
642
722
|
}
|
|
643
723
|
else if (relation.meta?.one_allowed_collections) {
|
|
724
|
+
ReadableCollectionQuantifierFilterTypes[relation.collection]?.removeField('item');
|
|
644
725
|
ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
|
|
645
726
|
for (const collection of relation.meta.one_allowed_collections) {
|
|
727
|
+
ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
|
|
728
|
+
[`item__${collection}`]: ReadableCollectionFilterTypes[collection],
|
|
729
|
+
});
|
|
646
730
|
ReadableCollectionFilterTypes[relation.collection]?.addFields({
|
|
647
731
|
[`item__${collection}`]: ReadableCollectionFilterTypes[collection],
|
|
648
732
|
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { GraphQLSchema } from 'graphql';
|
|
2
|
-
import
|
|
3
|
-
export declare const cache:
|
|
2
|
+
import { LRUMap } from 'mnemonist';
|
|
3
|
+
export declare const cache: LRUMap<string, string | GraphQLSchema>;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { GraphQLSchema } from 'graphql';
|
|
3
|
-
import
|
|
3
|
+
import { LRUMap } from 'mnemonist';
|
|
4
4
|
import { useBus } from '../../bus/index.js';
|
|
5
|
-
// Workaround for misaligned types in mnemonist package exports
|
|
6
|
-
const LRUMap = LRUMapDefault;
|
|
7
5
|
const env = useEnv();
|
|
8
6
|
const bus = useBus();
|
|
9
7
|
export const cache = new LRUMap(Number(env['GRAPHQL_SCHEMA_CACHE_CAPACITY'] ?? 100));
|
|
@@ -2,19 +2,19 @@ import type { GraphQLService } from './index.js';
|
|
|
2
2
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
3
3
|
export declare function bindPubSub(): void;
|
|
4
4
|
export declare function createSubscriptionGenerator(gql: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
|
|
5
|
-
[
|
|
5
|
+
[event]: {
|
|
6
6
|
key: string | number;
|
|
7
7
|
data: null;
|
|
8
8
|
event: "delete";
|
|
9
9
|
};
|
|
10
10
|
} | {
|
|
11
|
-
[
|
|
11
|
+
[event]: {
|
|
12
12
|
key: string | number;
|
|
13
13
|
data: any;
|
|
14
14
|
event: "create";
|
|
15
15
|
};
|
|
16
16
|
} | {
|
|
17
|
-
[
|
|
17
|
+
[event]: {
|
|
18
18
|
key: string | number;
|
|
19
19
|
data: any;
|
|
20
20
|
event: "update";
|
|
@@ -12,7 +12,7 @@ export function bindPubSub() {
|
|
|
12
12
|
}
|
|
13
13
|
export function createSubscriptionGenerator(gql, event) {
|
|
14
14
|
return async function* (_x, _y, _z, request) {
|
|
15
|
-
const fields = parseFields(gql, request);
|
|
15
|
+
const fields = await parseFields(gql, request);
|
|
16
16
|
const args = parseArguments(request);
|
|
17
17
|
for await (const payload of messages.subscribe(event)) {
|
|
18
18
|
const eventData = payload;
|
|
@@ -79,7 +79,7 @@ function createPubSub(emitter) {
|
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
function parseFields(gql, request) {
|
|
82
|
+
async function parseFields(gql, request) {
|
|
83
83
|
const selections = request.fieldNodes[0]?.selectionSet?.selections ?? [];
|
|
84
84
|
const dataSelections = selections.reduce((result, selection) => {
|
|
85
85
|
if (selection.kind === 'Field' &&
|
|
@@ -89,7 +89,7 @@ function parseFields(gql, request) {
|
|
|
89
89
|
}
|
|
90
90
|
return result;
|
|
91
91
|
}, []);
|
|
92
|
-
const { fields } = getQuery({}, dataSelections, request.variableValues, gql.accountability);
|
|
92
|
+
const { fields } = await getQuery({}, dataSelections, request.variableValues, gql.schema, gql.accountability);
|
|
93
93
|
return fields ?? [];
|
|
94
94
|
}
|
|
95
95
|
function parseArguments(request) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Accountability, Query } from '@directus/types';
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { SelectionNode } from 'graphql';
|
|
3
3
|
/**
|
|
4
4
|
* Resolve the aggregation query based on the requested aggregated fields
|
|
5
5
|
*/
|
|
6
|
-
export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], accountability?: Accountability | null): Query
|
|
6
|
+
export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { replaceFuncs } from './replace-funcs.js';
|
|
2
1
|
import { sanitizeQuery } from '../../../utils/sanitize-query.js';
|
|
3
2
|
import { validateQuery } from '../../../utils/validate-query.js';
|
|
3
|
+
import { replaceFuncs } from './replace-funcs.js';
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the aggregation query based on the requested aggregated fields
|
|
6
6
|
*/
|
|
7
|
-
export function getAggregateQuery(rawQuery, selections, accountability) {
|
|
8
|
-
const query = sanitizeQuery(rawQuery, accountability);
|
|
7
|
+
export async function getAggregateQuery(rawQuery, selections, schema, accountability) {
|
|
8
|
+
const query = await sanitizeQuery(rawQuery, schema, accountability);
|
|
9
9
|
query.aggregate = {};
|
|
10
10
|
for (let aggregationGroup of selections) {
|
|
11
11
|
if ((aggregationGroup.kind === 'Field') !== true)
|
package/dist/services/items.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class ItemsService<Item extends AnyItem = AnyItem, Collection ext
|
|
|
18
18
|
eventScope: string;
|
|
19
19
|
schema: SchemaOverview;
|
|
20
20
|
cache: Keyv<any> | null;
|
|
21
|
+
nested: string[];
|
|
21
22
|
constructor(collection: Collection, options: AbstractServiceOptions);
|
|
22
23
|
/**
|
|
23
24
|
* Create a fork of the current service, allowing instantiation with different options.
|
package/dist/services/items.js
CHANGED
|
@@ -26,6 +26,7 @@ export class ItemsService {
|
|
|
26
26
|
eventScope;
|
|
27
27
|
schema;
|
|
28
28
|
cache;
|
|
29
|
+
nested;
|
|
29
30
|
constructor(collection, options) {
|
|
30
31
|
this.collection = collection;
|
|
31
32
|
this.knex = options.knex || getDatabase();
|
|
@@ -33,6 +34,7 @@ export class ItemsService {
|
|
|
33
34
|
this.eventScope = isSystemCollection(this.collection) ? this.collection.substring(9) : 'items';
|
|
34
35
|
this.schema = options.schema;
|
|
35
36
|
this.cache = getCache().cache;
|
|
37
|
+
this.nested = options.nested ?? [];
|
|
36
38
|
return this;
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
@@ -43,7 +45,13 @@ export class ItemsService {
|
|
|
43
45
|
// ItemsService expects `collection` and `options` as parameters,
|
|
44
46
|
// while the other services only expect `options`
|
|
45
47
|
const isItemsService = Service.length === 2;
|
|
46
|
-
const newOptions = {
|
|
48
|
+
const newOptions = {
|
|
49
|
+
knex: this.knex,
|
|
50
|
+
accountability: this.accountability,
|
|
51
|
+
schema: this.schema,
|
|
52
|
+
nested: this.nested,
|
|
53
|
+
...options,
|
|
54
|
+
};
|
|
47
55
|
if (isItemsService) {
|
|
48
56
|
return new ItemsService(this.collection, newOptions);
|
|
49
57
|
}
|
|
@@ -95,19 +103,15 @@ export class ItemsService {
|
|
|
95
103
|
.filter((field) => field.alias === true)
|
|
96
104
|
.map((field) => field.field);
|
|
97
105
|
const payload = cloneDeep(data);
|
|
106
|
+
let actionHookPayload = payload;
|
|
98
107
|
const nestedActionEvents = [];
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
/**
|
|
109
|
+
* By wrapping the logic in a transaction, we make sure we automatically roll back all the
|
|
110
|
+
* changes in the DB if any of the parts contained within throws an error. This also means
|
|
111
|
+
* that any errors thrown in any nested relational changes will bubble up and cancel the whole
|
|
112
|
+
* update tree
|
|
113
|
+
*/
|
|
103
114
|
const primaryKey = await transaction(this.knex, async (trx) => {
|
|
104
|
-
const serviceOptions = {
|
|
105
|
-
accountability: this.accountability,
|
|
106
|
-
knex: trx,
|
|
107
|
-
schema: this.schema,
|
|
108
|
-
};
|
|
109
|
-
// We're creating new services instances so they can use the transaction as their Knex interface
|
|
110
|
-
const payloadService = new PayloadService(this.collection, serviceOptions);
|
|
111
115
|
// Run all hooks that are attached to this event so the end user has the chance to augment the
|
|
112
116
|
// item that is about to be saved
|
|
113
117
|
const payloadAfterHooks = opts.emitEvents !== false
|
|
@@ -127,6 +131,7 @@ export class ItemsService {
|
|
|
127
131
|
action: 'create',
|
|
128
132
|
collection: this.collection,
|
|
129
133
|
payload: payloadAfterHooks,
|
|
134
|
+
nested: this.nested,
|
|
130
135
|
}, {
|
|
131
136
|
knex: trx,
|
|
132
137
|
schema: this.schema,
|
|
@@ -135,6 +140,15 @@ export class ItemsService {
|
|
|
135
140
|
if (opts.preMutationError) {
|
|
136
141
|
throw opts.preMutationError;
|
|
137
142
|
}
|
|
143
|
+
// Ensure the action hook payload has the post filter hook + preset changes
|
|
144
|
+
actionHookPayload = payloadWithPresets;
|
|
145
|
+
// We're creating new services instances so they can use the transaction as their Knex interface
|
|
146
|
+
const payloadService = new PayloadService(this.collection, {
|
|
147
|
+
accountability: this.accountability,
|
|
148
|
+
knex: trx,
|
|
149
|
+
schema: this.schema,
|
|
150
|
+
nested: this.nested,
|
|
151
|
+
});
|
|
138
152
|
const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
|
|
139
153
|
const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
|
|
140
154
|
const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
|
|
@@ -190,7 +204,7 @@ export class ItemsService {
|
|
|
190
204
|
primaryKey = result.id;
|
|
191
205
|
// Set the primary key on the input item, in order for the "after" event hook to be able
|
|
192
206
|
// to read from it
|
|
193
|
-
|
|
207
|
+
actionHookPayload[primaryKeyField] = primaryKey;
|
|
194
208
|
}
|
|
195
209
|
// At this point, the primary key is guaranteed to be set.
|
|
196
210
|
primaryKey = primaryKey;
|
|
@@ -260,7 +274,7 @@ export class ItemsService {
|
|
|
260
274
|
? ['items.create', `${this.collection}.items.create`]
|
|
261
275
|
: `${this.eventScope}.create`,
|
|
262
276
|
meta: {
|
|
263
|
-
payload,
|
|
277
|
+
payload: actionHookPayload,
|
|
264
278
|
key: primaryKey,
|
|
265
279
|
collection: this.collection,
|
|
266
280
|
},
|
|
@@ -450,8 +464,6 @@ export class ItemsService {
|
|
|
450
464
|
* Uses `this.updateMany` under the hood.
|
|
451
465
|
*/
|
|
452
466
|
async updateOne(key, data, opts) {
|
|
453
|
-
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
454
|
-
validateKeys(this.schema, this.collection, primaryKeyField, key);
|
|
455
467
|
await this.updateMany([key], data, opts);
|
|
456
468
|
return key;
|
|
457
469
|
}
|
|
@@ -553,6 +565,7 @@ export class ItemsService {
|
|
|
553
565
|
action: 'update',
|
|
554
566
|
collection: this.collection,
|
|
555
567
|
payload: payloadAfterHooks,
|
|
568
|
+
nested: this.nested,
|
|
556
569
|
}, {
|
|
557
570
|
knex: this.knex,
|
|
558
571
|
schema: this.schema,
|
|
@@ -566,6 +579,7 @@ export class ItemsService {
|
|
|
566
579
|
accountability: this.accountability,
|
|
567
580
|
knex: trx,
|
|
568
581
|
schema: this.schema,
|
|
582
|
+
nested: this.nested,
|
|
569
583
|
});
|
|
570
584
|
const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
|
|
571
585
|
const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
|
package/dist/services/meta.js
CHANGED
|
@@ -33,9 +33,10 @@ export class MetaService {
|
|
|
33
33
|
return this.filterCount(collection, {});
|
|
34
34
|
}
|
|
35
35
|
async filterCount(collection, query) {
|
|
36
|
+
const primaryKeyName = this.schema.collections[collection].primary;
|
|
36
37
|
const aggregateQuery = {
|
|
37
38
|
aggregate: {
|
|
38
|
-
|
|
39
|
+
countDistinct: [primaryKeyName],
|
|
39
40
|
},
|
|
40
41
|
search: query.search ?? null,
|
|
41
42
|
filter: query.filter ?? null,
|
|
@@ -52,6 +53,7 @@ export class MetaService {
|
|
|
52
53
|
const records = await runAst(ast, this.schema, this.accountability, {
|
|
53
54
|
knex: this.knex,
|
|
54
55
|
});
|
|
55
|
-
return Number((isArray(records) ? records[0]?.['
|
|
56
|
+
return Number((isArray(records) ? records[0]?.['countDistinct'][primaryKeyName] : records?.['countDistinct'][primaryKeyName]) ??
|
|
57
|
+
0);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -29,6 +29,7 @@ export declare class PayloadService {
|
|
|
29
29
|
helpers: Helpers;
|
|
30
30
|
collection: string;
|
|
31
31
|
schema: SchemaOverview;
|
|
32
|
+
nested: string[];
|
|
32
33
|
constructor(collection: string, options: AbstractServiceOptions);
|
|
33
34
|
transformers: Transformers;
|
|
34
35
|
processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
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
|
}
|