@directus/api 24.0.1 → 25.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 +10 -4
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +20 -7
- package/dist/controllers/assets.js +2 -2
- package/dist/controllers/metrics.d.ts +2 -0
- package/dist/controllers/metrics.js +33 -0
- package/dist/controllers/server.js +1 -1
- package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
- package/dist/database/helpers/number/dialects/mssql.js +3 -3
- package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
- package/dist/database/helpers/number/dialects/oracle.js +2 -2
- package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
- package/dist/database/helpers/number/dialects/sqlite.js +2 -2
- package/dist/database/helpers/number/types.d.ts +2 -2
- package/dist/database/helpers/number/types.js +2 -2
- package/dist/database/index.js +3 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/lib/create-metrics.d.ts +15 -0
- package/dist/metrics/lib/create-metrics.js +239 -0
- package/dist/metrics/lib/use-metrics.d.ts +17 -0
- package/dist/metrics/lib/use-metrics.js +15 -0
- package/dist/metrics/types/metric.d.ts +1 -0
- package/dist/metrics/types/metric.js +1 -0
- package/dist/middleware/respond.js +7 -1
- package/dist/operations/condition/index.js +7 -2
- package/dist/schedules/metrics.d.ts +7 -0
- package/dist/schedules/metrics.js +44 -0
- package/dist/services/assets.d.ts +6 -1
- package/dist/services/assets.js +8 -6
- package/dist/services/fields.js +1 -1
- package/dist/services/graphql/errors/format.d.ts +6 -0
- package/dist/services/graphql/errors/format.js +14 -0
- package/dist/services/graphql/index.d.ts +5 -53
- package/dist/services/graphql/index.js +5 -2720
- package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
- package/dist/services/graphql/resolvers/mutation.js +74 -0
- package/dist/services/graphql/resolvers/query.d.ts +8 -0
- package/dist/services/graphql/resolvers/query.js +87 -0
- package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
- package/dist/services/graphql/resolvers/system-admin.js +236 -0
- package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
- package/dist/services/graphql/resolvers/system-global.js +435 -0
- package/dist/services/graphql/resolvers/system.d.ts +11 -0
- package/dist/services/graphql/resolvers/system.js +554 -0
- package/dist/services/graphql/schema/get-types.d.ts +12 -0
- package/dist/services/graphql/schema/get-types.js +223 -0
- package/dist/services/graphql/schema/index.d.ts +32 -0
- package/dist/services/graphql/schema/index.js +190 -0
- package/dist/services/graphql/schema/parse-args.d.ts +9 -0
- package/dist/services/graphql/schema/parse-args.js +35 -0
- package/dist/services/graphql/schema/parse-query.d.ts +7 -0
- package/dist/services/graphql/schema/parse-query.js +98 -0
- package/dist/services/graphql/schema/read.d.ts +12 -0
- package/dist/services/graphql/schema/read.js +653 -0
- package/dist/services/graphql/schema/write.d.ts +9 -0
- package/dist/services/graphql/schema/write.js +142 -0
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/subscription.js +7 -6
- package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
- package/dist/services/graphql/utils/aggrgate-query.js +32 -0
- package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
- package/dist/services/graphql/utils/replace-fragments.js +21 -0
- package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
- package/dist/services/graphql/utils/replace-funcs.js +21 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
- package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
- package/dist/services/items.js +0 -2
- package/dist/services/meta.js +25 -84
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +23 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +58 -21
- package/dist/utils/freeze-schema.d.ts +3 -0
- package/dist/utils/freeze-schema.js +31 -0
- package/dist/utils/get-accountability-for-token.js +1 -0
- package/dist/utils/get-milliseconds.js +1 -1
- package/dist/utils/get-schema.js +10 -5
- package/dist/utils/permissions-cachable.d.ts +8 -0
- package/dist/utils/permissions-cachable.js +38 -0
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/messages.d.ts +6 -6
- package/package.json +22 -19
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull } from 'graphql';
|
|
2
|
+
import { ObjectTypeComposer, toInputObjectType } from 'graphql-compose';
|
|
3
|
+
import { GraphQLService } from '../index.js';
|
|
4
|
+
import { resolveMutation } from '../resolvers/mutation.js';
|
|
5
|
+
import { getTypes } from './get-types.js';
|
|
6
|
+
import { SYSTEM_DENY_LIST } from './index.js';
|
|
7
|
+
export function getWritableTypes(gql, schemaComposer, schema, inconsistentFields, ReadCollectionTypes) {
|
|
8
|
+
const { CollectionTypes: CreateCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'create');
|
|
9
|
+
const { CollectionTypes: UpdateCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'update');
|
|
10
|
+
const DeleteCollectionTypes = {};
|
|
11
|
+
for (const collection of Object.values(schema.create.collections)) {
|
|
12
|
+
if (Object.keys(collection.fields).length === 0)
|
|
13
|
+
continue;
|
|
14
|
+
if (SYSTEM_DENY_LIST.includes(collection.collection))
|
|
15
|
+
continue;
|
|
16
|
+
if (collection.collection in CreateCollectionTypes === false)
|
|
17
|
+
continue;
|
|
18
|
+
const collectionIsReadable = collection.collection in ReadCollectionTypes;
|
|
19
|
+
const creatableFields = CreateCollectionTypes[collection.collection]?.getFields() || {};
|
|
20
|
+
if (Object.keys(creatableFields).length > 0) {
|
|
21
|
+
const resolverDefinition = {
|
|
22
|
+
name: `create_${collection.collection}_items`,
|
|
23
|
+
type: collectionIsReadable
|
|
24
|
+
? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
|
|
25
|
+
: GraphQLBoolean,
|
|
26
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
27
|
+
};
|
|
28
|
+
if (collectionIsReadable) {
|
|
29
|
+
resolverDefinition.args = ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs();
|
|
30
|
+
}
|
|
31
|
+
CreateCollectionTypes[collection.collection].addResolver(resolverDefinition);
|
|
32
|
+
CreateCollectionTypes[collection.collection].addResolver({
|
|
33
|
+
name: `create_${collection.collection}_item`,
|
|
34
|
+
type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
|
|
35
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
36
|
+
});
|
|
37
|
+
CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_items`).addArgs({
|
|
38
|
+
...CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_items`).getArgs(),
|
|
39
|
+
data: [
|
|
40
|
+
toInputObjectType(CreateCollectionTypes[collection.collection]).setTypeName(`create_${collection.collection}_input`).NonNull,
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_item`).addArgs({
|
|
44
|
+
...CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_item`).getArgs(),
|
|
45
|
+
data: toInputObjectType(CreateCollectionTypes[collection.collection]).setTypeName(`create_${collection.collection}_input`).NonNull,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const collection of Object.values(schema.update.collections)) {
|
|
50
|
+
if (Object.keys(collection.fields).length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
if (SYSTEM_DENY_LIST.includes(collection.collection))
|
|
53
|
+
continue;
|
|
54
|
+
if (collection.collection in UpdateCollectionTypes === false)
|
|
55
|
+
continue;
|
|
56
|
+
const collectionIsReadable = collection.collection in ReadCollectionTypes;
|
|
57
|
+
const updatableFields = UpdateCollectionTypes[collection.collection]?.getFields() || {};
|
|
58
|
+
if (Object.keys(updatableFields).length > 0) {
|
|
59
|
+
if (collection.singleton) {
|
|
60
|
+
UpdateCollectionTypes[collection.collection].addResolver({
|
|
61
|
+
name: `update_${collection.collection}`,
|
|
62
|
+
type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
|
|
63
|
+
args: {
|
|
64
|
+
data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
|
|
65
|
+
},
|
|
66
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
UpdateCollectionTypes[collection.collection].addResolver({
|
|
71
|
+
name: `update_${collection.collection}_batch`,
|
|
72
|
+
type: collectionIsReadable
|
|
73
|
+
? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
|
|
74
|
+
: GraphQLBoolean,
|
|
75
|
+
args: {
|
|
76
|
+
...(collectionIsReadable
|
|
77
|
+
? ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs()
|
|
78
|
+
: {}),
|
|
79
|
+
data: [
|
|
80
|
+
toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
84
|
+
});
|
|
85
|
+
UpdateCollectionTypes[collection.collection].addResolver({
|
|
86
|
+
name: `update_${collection.collection}_items`,
|
|
87
|
+
type: collectionIsReadable
|
|
88
|
+
? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
|
|
89
|
+
: GraphQLBoolean,
|
|
90
|
+
args: {
|
|
91
|
+
...(collectionIsReadable
|
|
92
|
+
? ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs()
|
|
93
|
+
: {}),
|
|
94
|
+
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
|
|
95
|
+
data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
|
|
96
|
+
},
|
|
97
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
98
|
+
});
|
|
99
|
+
UpdateCollectionTypes[collection.collection].addResolver({
|
|
100
|
+
name: `update_${collection.collection}_item`,
|
|
101
|
+
type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
|
|
102
|
+
args: {
|
|
103
|
+
id: new GraphQLNonNull(GraphQLID),
|
|
104
|
+
data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
|
|
105
|
+
},
|
|
106
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
DeleteCollectionTypes['many'] = schemaComposer.createObjectTC({
|
|
112
|
+
name: `delete_many`,
|
|
113
|
+
fields: {
|
|
114
|
+
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
DeleteCollectionTypes['one'] = schemaComposer.createObjectTC({
|
|
118
|
+
name: `delete_one`,
|
|
119
|
+
fields: {
|
|
120
|
+
id: new GraphQLNonNull(GraphQLID),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
for (const collection of Object.values(schema.delete.collections)) {
|
|
124
|
+
DeleteCollectionTypes['many'].addResolver({
|
|
125
|
+
name: `delete_${collection.collection}_items`,
|
|
126
|
+
type: DeleteCollectionTypes['many'],
|
|
127
|
+
args: {
|
|
128
|
+
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
|
|
129
|
+
},
|
|
130
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
131
|
+
});
|
|
132
|
+
DeleteCollectionTypes['one'].addResolver({
|
|
133
|
+
name: `delete_${collection.collection}_item`,
|
|
134
|
+
type: DeleteCollectionTypes['one'],
|
|
135
|
+
args: {
|
|
136
|
+
id: new GraphQLNonNull(GraphQLID),
|
|
137
|
+
},
|
|
138
|
+
resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes };
|
|
142
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GraphQLService } from './index.js';
|
|
2
2
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
3
3
|
export declare function bindPubSub(): void;
|
|
4
|
-
export declare function createSubscriptionGenerator(
|
|
4
|
+
export declare function createSubscriptionGenerator(gql: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
|
|
5
5
|
[x: string]: {
|
|
6
6
|
key: string | number;
|
|
7
7
|
data: null;
|
|
@@ -2,6 +2,7 @@ import { EventEmitter, on } from 'events';
|
|
|
2
2
|
import { useBus } from '../../bus/index.js';
|
|
3
3
|
import { getSchema } from '../../utils/get-schema.js';
|
|
4
4
|
import { getPayload } from '../../websocket/utils/items.js';
|
|
5
|
+
import { getQuery } from './schema/parse-query.js';
|
|
5
6
|
const messages = createPubSub(new EventEmitter());
|
|
6
7
|
export function bindPubSub() {
|
|
7
8
|
const messenger = useBus();
|
|
@@ -9,9 +10,9 @@ export function bindPubSub() {
|
|
|
9
10
|
messages.publish(`${message['collection']}_mutated`, message);
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
|
-
export function createSubscriptionGenerator(
|
|
13
|
+
export function createSubscriptionGenerator(gql, event) {
|
|
13
14
|
return async function* (_x, _y, _z, request) {
|
|
14
|
-
const fields = parseFields(
|
|
15
|
+
const fields = parseFields(gql, request);
|
|
15
16
|
const args = parseArguments(request);
|
|
16
17
|
for await (const payload of messages.subscribe(event)) {
|
|
17
18
|
const eventData = payload;
|
|
@@ -33,7 +34,7 @@ export function createSubscriptionGenerator(self, event) {
|
|
|
33
34
|
if (eventData['action'] === 'create') {
|
|
34
35
|
try {
|
|
35
36
|
subscription.item = eventData['key'];
|
|
36
|
-
const result = await getPayload(subscription,
|
|
37
|
+
const result = await getPayload(subscription, gql.accountability, schema, eventData);
|
|
37
38
|
yield {
|
|
38
39
|
[event]: {
|
|
39
40
|
key: eventData['key'],
|
|
@@ -50,7 +51,7 @@ export function createSubscriptionGenerator(self, event) {
|
|
|
50
51
|
for (const key of eventData['keys']) {
|
|
51
52
|
try {
|
|
52
53
|
subscription.item = key;
|
|
53
|
-
const result = await getPayload(subscription,
|
|
54
|
+
const result = await getPayload(subscription, gql.accountability, schema, eventData);
|
|
54
55
|
yield {
|
|
55
56
|
[event]: {
|
|
56
57
|
key,
|
|
@@ -78,7 +79,7 @@ function createPubSub(emitter) {
|
|
|
78
79
|
},
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
|
-
function parseFields(
|
|
82
|
+
function parseFields(gql, request) {
|
|
82
83
|
const selections = request.fieldNodes[0]?.selectionSet?.selections ?? [];
|
|
83
84
|
const dataSelections = selections.reduce((result, selection) => {
|
|
84
85
|
if (selection.kind === 'Field' &&
|
|
@@ -88,7 +89,7 @@ function parseFields(service, request) {
|
|
|
88
89
|
}
|
|
89
90
|
return result;
|
|
90
91
|
}, []);
|
|
91
|
-
const { fields } =
|
|
92
|
+
const { fields } = getQuery({}, dataSelections, request.variableValues, gql.accountability);
|
|
92
93
|
return fields ?? [];
|
|
93
94
|
}
|
|
94
95
|
function parseArguments(request) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Accountability, Query } from '@directus/types';
|
|
2
|
+
import type { SelectionNode } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the aggregation query based on the requested aggregated fields
|
|
5
|
+
*/
|
|
6
|
+
export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], accountability?: Accountability | null): Query;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { replaceFuncs } from './replace-funcs.js';
|
|
2
|
+
import { sanitizeQuery } from '../../../utils/sanitize-query.js';
|
|
3
|
+
import { validateQuery } from '../../../utils/validate-query.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the aggregation query based on the requested aggregated fields
|
|
6
|
+
*/
|
|
7
|
+
export function getAggregateQuery(rawQuery, selections, accountability) {
|
|
8
|
+
const query = sanitizeQuery(rawQuery, accountability);
|
|
9
|
+
query.aggregate = {};
|
|
10
|
+
for (let aggregationGroup of selections) {
|
|
11
|
+
if ((aggregationGroup.kind === 'Field') !== true)
|
|
12
|
+
continue;
|
|
13
|
+
aggregationGroup = aggregationGroup;
|
|
14
|
+
// filter out graphql pointers, like __typename
|
|
15
|
+
if (aggregationGroup.name.value.startsWith('__'))
|
|
16
|
+
continue;
|
|
17
|
+
const aggregateProperty = aggregationGroup.name.value;
|
|
18
|
+
query.aggregate[aggregateProperty] =
|
|
19
|
+
aggregationGroup.selectionSet?.selections
|
|
20
|
+
// filter out graphql pointers, like __typename
|
|
21
|
+
.filter((selectionNode) => !selectionNode?.name.value.startsWith('__'))
|
|
22
|
+
.map((selectionNode) => {
|
|
23
|
+
selectionNode = selectionNode;
|
|
24
|
+
return selectionNode.name.value;
|
|
25
|
+
}) ?? [];
|
|
26
|
+
}
|
|
27
|
+
if (query.filter) {
|
|
28
|
+
query.filter = replaceFuncs(query.filter);
|
|
29
|
+
}
|
|
30
|
+
validateQuery(query);
|
|
31
|
+
return query;
|
|
32
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FragmentDefinitionNode, SelectionNode } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Replace all fragments in a selectionset for the actual selection set as defined in the fragment
|
|
4
|
+
* Effectively merges the selections with the fragments used in those selections
|
|
5
|
+
*/
|
|
6
|
+
export declare function replaceFragmentsInSelections(selections: readonly SelectionNode[] | undefined, fragments: Record<string, FragmentDefinitionNode>): readonly SelectionNode[] | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { flatten } from 'lodash-es';
|
|
2
|
+
/**
|
|
3
|
+
* Replace all fragments in a selectionset for the actual selection set as defined in the fragment
|
|
4
|
+
* Effectively merges the selections with the fragments used in those selections
|
|
5
|
+
*/
|
|
6
|
+
export function replaceFragmentsInSelections(selections, fragments) {
|
|
7
|
+
if (!selections)
|
|
8
|
+
return null;
|
|
9
|
+
const result = flatten(selections.map((selection) => {
|
|
10
|
+
// Fragments can contains fragments themselves. This allows for nested fragments
|
|
11
|
+
if (selection.kind === 'FragmentSpread') {
|
|
12
|
+
return replaceFragmentsInSelections(fragments[selection.name.value].selectionSet.selections, fragments);
|
|
13
|
+
}
|
|
14
|
+
// Nested relational fields can also contain fragments
|
|
15
|
+
if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') && selection.selectionSet) {
|
|
16
|
+
selection.selectionSet.selections = replaceFragmentsInSelections(selection.selectionSet.selections, fragments);
|
|
17
|
+
}
|
|
18
|
+
return selection;
|
|
19
|
+
})).filter((s) => s);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FUNCTIONS } from '@directus/constants';
|
|
2
|
+
import { transform } from 'lodash-es';
|
|
3
|
+
/**
|
|
4
|
+
* Replace functions from GraphQL format to Directus-Filter format
|
|
5
|
+
*/
|
|
6
|
+
export function replaceFuncs(filter) {
|
|
7
|
+
return replaceFuncDeep(filter);
|
|
8
|
+
function replaceFuncDeep(filter) {
|
|
9
|
+
return transform(filter, (result, value, key) => {
|
|
10
|
+
const isFunctionKey = typeof key === 'string' && key.endsWith('_func') && FUNCTIONS.includes(Object.keys(value)[0]);
|
|
11
|
+
if (isFunctionKey) {
|
|
12
|
+
const functionName = Object.keys(value)[0];
|
|
13
|
+
const fieldName = key.slice(0, -5);
|
|
14
|
+
result[`${functionName}(${fieldName})`] = Object.values(value)[0];
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
result[key] = value?.constructor === Object || value?.constructor === Array ? replaceFuncDeep(value) : value;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -5,4 +5,4 @@ import type { SchemaOverview } from '@directus/types';
|
|
|
5
5
|
* @param schema
|
|
6
6
|
* @returns sanitized schema
|
|
7
7
|
*/
|
|
8
|
-
export declare function sanitizeGraphqlSchema(schema: SchemaOverview): SchemaOverview;
|
|
8
|
+
export declare function sanitizeGraphqlSchema(schema: Readonly<SchemaOverview>): SchemaOverview;
|
|
@@ -36,7 +36,7 @@ const GRAPHQL_RESERVED_NAMES = [
|
|
|
36
36
|
*/
|
|
37
37
|
export function sanitizeGraphqlSchema(schema) {
|
|
38
38
|
const logger = useLogger();
|
|
39
|
-
const
|
|
39
|
+
const collectionEntries = Object.entries(schema.collections).filter(([collectionName, _data]) => {
|
|
40
40
|
// double underscore __ is reserved for GraphQL introspection
|
|
41
41
|
if (collectionName.startsWith('__') || !collectionName.match(GRAPHQL_NAME_REGEX)) {
|
|
42
42
|
logger.warn(`GraphQL skipping collection "${collectionName}" because it is not a valid name matching /^[_A-Za-z][_0-9A-Za-z]*$/ or starts with __`);
|
|
@@ -48,14 +48,14 @@ export function sanitizeGraphqlSchema(schema) {
|
|
|
48
48
|
}
|
|
49
49
|
return true;
|
|
50
50
|
});
|
|
51
|
-
|
|
52
|
-
const collectionExists = (collection) => Boolean(
|
|
51
|
+
const collections = Object.fromEntries(collectionEntries);
|
|
52
|
+
const collectionExists = (collection) => Boolean(collections[collection]);
|
|
53
53
|
const skipRelation = (relation) => {
|
|
54
54
|
const relationName = relation.schema?.constraint_name ?? `${relation.collection}.${relation.field}`;
|
|
55
55
|
logger.warn(`GraphQL skipping relation "${relationName}" because it links to a non-existent or invalid collection.`);
|
|
56
56
|
return false;
|
|
57
57
|
};
|
|
58
|
-
|
|
58
|
+
const relations = schema.relations.filter((relation) => {
|
|
59
59
|
if (relation.collection && !collectionExists(relation.collection)) {
|
|
60
60
|
return skipRelation(relation);
|
|
61
61
|
}
|
|
@@ -76,5 +76,5 @@ export function sanitizeGraphqlSchema(schema) {
|
|
|
76
76
|
}
|
|
77
77
|
return true;
|
|
78
78
|
});
|
|
79
|
-
return
|
|
79
|
+
return { collections, relations };
|
|
80
80
|
}
|
package/dist/services/items.js
CHANGED
|
@@ -442,8 +442,6 @@ export class ItemsService {
|
|
|
442
442
|
*/
|
|
443
443
|
async updateByQuery(query, data, opts) {
|
|
444
444
|
const keys = await this.getKeysByQuery(query);
|
|
445
|
-
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
446
|
-
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
|
447
445
|
return keys.length ? await this.updateMany(keys, data, opts) : [];
|
|
448
446
|
}
|
|
449
447
|
/**
|
package/dist/services/meta.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { isArray } from 'lodash-es';
|
|
2
|
+
import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
|
|
1
3
|
import getDatabase from '../database/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { dedupeAccess } from '../permissions/modules/process-ast/utils/dedupe-access.js';
|
|
5
|
-
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
6
|
-
import { applyFilter, applySearch } from '../utils/apply-query.js';
|
|
4
|
+
import { runAst } from '../database/run-ast/run-ast.js';
|
|
5
|
+
import { processAst } from '../permissions/modules/process-ast/process-ast.js';
|
|
7
6
|
export class MetaService {
|
|
8
7
|
knex;
|
|
9
8
|
accountability;
|
|
@@ -31,86 +30,28 @@ export class MetaService {
|
|
|
31
30
|
}, {});
|
|
32
31
|
}
|
|
33
32
|
async totalCount(collection) {
|
|
34
|
-
|
|
35
|
-
let hasJoins = false;
|
|
36
|
-
if (this.accountability && this.accountability.admin === false) {
|
|
37
|
-
const context = { knex: this.knex, schema: this.schema };
|
|
38
|
-
await validateAccess({
|
|
39
|
-
accountability: this.accountability,
|
|
40
|
-
action: 'read',
|
|
41
|
-
collection,
|
|
42
|
-
}, context);
|
|
43
|
-
const policies = await fetchPolicies(this.accountability, context);
|
|
44
|
-
const permissions = await fetchPermissions({
|
|
45
|
-
action: 'read',
|
|
46
|
-
policies,
|
|
47
|
-
accountability: this.accountability,
|
|
48
|
-
}, context);
|
|
49
|
-
const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
|
|
50
|
-
const rules = dedupeAccess(collectionPermissions);
|
|
51
|
-
const cases = rules.map(({ rule }) => rule);
|
|
52
|
-
const filter = {
|
|
53
|
-
_or: cases,
|
|
54
|
-
};
|
|
55
|
-
const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions);
|
|
56
|
-
hasJoins = result.hasJoins;
|
|
57
|
-
}
|
|
58
|
-
if (hasJoins) {
|
|
59
|
-
const primaryKeyName = this.schema.collections[collection].primary;
|
|
60
|
-
dbQuery.countDistinct({ count: [`${collection}.${primaryKeyName}`] });
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
dbQuery.count('*', { as: 'count' });
|
|
64
|
-
}
|
|
65
|
-
const result = await dbQuery.first();
|
|
66
|
-
return Number(result?.count ?? 0);
|
|
33
|
+
return this.filterCount(collection, {});
|
|
67
34
|
}
|
|
68
35
|
async filterCount(collection, query) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cases = rules.map(({ rule }) => rule);
|
|
90
|
-
const permissionsFilter = {
|
|
91
|
-
_or: cases,
|
|
92
|
-
};
|
|
93
|
-
if (Object.keys(filter).length > 0) {
|
|
94
|
-
filter = { _and: [permissionsFilter, filter] };
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
filter = permissionsFilter;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (Object.keys(filter).length > 0) {
|
|
101
|
-
({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions));
|
|
102
|
-
}
|
|
103
|
-
if (query.search) {
|
|
104
|
-
applySearch(this.knex, this.schema, dbQuery, query.search, collection);
|
|
105
|
-
}
|
|
106
|
-
if (hasJoins) {
|
|
107
|
-
const primaryKeyName = this.schema.collections[collection].primary;
|
|
108
|
-
dbQuery.countDistinct({ count: [`${collection}.${primaryKeyName}`] });
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
dbQuery.count('*', { as: 'count' });
|
|
112
|
-
}
|
|
113
|
-
const result = await dbQuery.first();
|
|
114
|
-
return Number(result?.count ?? 0);
|
|
36
|
+
const aggregateQuery = {
|
|
37
|
+
aggregate: {
|
|
38
|
+
count: ['*'],
|
|
39
|
+
},
|
|
40
|
+
search: query.search ?? null,
|
|
41
|
+
filter: query.filter ?? null,
|
|
42
|
+
};
|
|
43
|
+
let ast = await getAstFromQuery({
|
|
44
|
+
collection,
|
|
45
|
+
query: aggregateQuery,
|
|
46
|
+
accountability: this.accountability,
|
|
47
|
+
}, {
|
|
48
|
+
schema: this.schema,
|
|
49
|
+
knex: this.knex,
|
|
50
|
+
});
|
|
51
|
+
ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
|
|
52
|
+
const records = await runAst(ast, this.schema, this.accountability, {
|
|
53
|
+
knex: this.knex,
|
|
54
|
+
});
|
|
55
|
+
return Number((isArray(records) ? records[0]?.['count'] : records?.['count']) ?? 0);
|
|
115
56
|
}
|
|
116
57
|
}
|
package/dist/services/users.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare class UsersService extends ItemsService {
|
|
|
13
13
|
* directus_settings.auth_password_policy
|
|
14
14
|
*/
|
|
15
15
|
private checkPasswordPolicy;
|
|
16
|
+
/**
|
|
17
|
+
* Clear users' sessions to log them out
|
|
18
|
+
*/
|
|
19
|
+
private clearUserSessions;
|
|
16
20
|
/**
|
|
17
21
|
* Get basic information of user identified by email
|
|
18
22
|
*/
|
package/dist/services/users.js
CHANGED
|
@@ -87,6 +87,21 @@ export class UsersService extends ItemsService {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Clear users' sessions to log them out
|
|
92
|
+
*/
|
|
93
|
+
async clearUserSessions(userKeys, excludeSession) {
|
|
94
|
+
if (excludeSession) {
|
|
95
|
+
await this.knex
|
|
96
|
+
.from('directus_sessions')
|
|
97
|
+
.whereIn('user', userKeys)
|
|
98
|
+
.andWhereNot('token', '=', excludeSession)
|
|
99
|
+
.delete();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await this.knex.from('directus_sessions').whereIn('user', userKeys).delete();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
90
105
|
/**
|
|
91
106
|
* Get basic information of user identified by email
|
|
92
107
|
*/
|
|
@@ -238,6 +253,12 @@ export class UsersService extends ItemsService {
|
|
|
238
253
|
opts.onRequireUserIntegrityCheck?.(opts.userIntegrityCheckFlags);
|
|
239
254
|
}
|
|
240
255
|
const result = await super.updateMany(keys, data, opts);
|
|
256
|
+
if (data['status'] !== undefined && data['status'] !== 'active') {
|
|
257
|
+
await this.clearUserSessions(keys);
|
|
258
|
+
}
|
|
259
|
+
else if (data['password'] !== undefined || data['email'] !== undefined) {
|
|
260
|
+
await this.clearUserSessions(keys, this.accountability?.session);
|
|
261
|
+
}
|
|
241
262
|
// Only clear the caches if the role has been updated
|
|
242
263
|
if ('role' in data) {
|
|
243
264
|
await this.clearCaches(opts);
|
|
@@ -264,6 +285,7 @@ export class UsersService extends ItemsService {
|
|
|
264
285
|
await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
|
|
265
286
|
await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
|
|
266
287
|
await super.deleteMany(keys, opts);
|
|
288
|
+
await this.clearUserSessions(keys);
|
|
267
289
|
return keys;
|
|
268
290
|
}
|
|
269
291
|
async inviteUser(email, role, url, subject) {
|
|
@@ -462,7 +484,7 @@ export class UsersService extends ItemsService {
|
|
|
462
484
|
await stall(STALL_TIME, timeStart);
|
|
463
485
|
}
|
|
464
486
|
async resetPassword(token, password) {
|
|
465
|
-
const { email, scope, hash } =
|
|
487
|
+
const { email, scope, hash } = verifyJWT(token, getSecret());
|
|
466
488
|
if (scope !== 'password-reset' || !hash)
|
|
467
489
|
throw new ForbiddenError();
|
|
468
490
|
const opts = {};
|
|
@@ -39,7 +39,7 @@ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuer
|
|
|
39
39
|
hasJoins: boolean;
|
|
40
40
|
hasMultiRelationalFilter: boolean;
|
|
41
41
|
};
|
|
42
|
-
export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): void;
|
|
42
|
+
export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string, aliasMap: AliasMap, permissions: Permission[]): void;
|
|
43
43
|
export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string, hasJoins: boolean): void;
|
|
44
44
|
export declare function joinFilterWithCases(filter: Filter | null | undefined, cases: Filter[]): Filter | null;
|
|
45
45
|
export {};
|