@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,223 @@
|
|
|
1
|
+
import { GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLScalarType, GraphQLUnionType } from 'graphql';
|
|
2
|
+
import { GraphQLJSON, ObjectTypeComposer } from 'graphql-compose';
|
|
3
|
+
import { mapKeys, pick } from 'lodash-es';
|
|
4
|
+
import { GENERATE_SPECIAL } from '../../../constants.js';
|
|
5
|
+
import { getGraphQLType } from '../../../utils/get-graphql-type.js';
|
|
6
|
+
import {} from '../index.js';
|
|
7
|
+
import { SYSTEM_DENY_LIST } from './index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Construct an object of types for every collection, using the permitted fields per action type
|
|
10
|
+
* as it's fields.
|
|
11
|
+
*/
|
|
12
|
+
export function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
|
|
13
|
+
const CollectionTypes = {};
|
|
14
|
+
const VersionTypes = {};
|
|
15
|
+
const CountFunctions = schemaComposer.createObjectTC({
|
|
16
|
+
name: 'count_functions',
|
|
17
|
+
fields: {
|
|
18
|
+
count: {
|
|
19
|
+
type: GraphQLInt,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const DateFunctions = schemaComposer.createObjectTC({
|
|
24
|
+
name: 'date_functions',
|
|
25
|
+
fields: {
|
|
26
|
+
year: {
|
|
27
|
+
type: GraphQLInt,
|
|
28
|
+
},
|
|
29
|
+
month: {
|
|
30
|
+
type: GraphQLInt,
|
|
31
|
+
},
|
|
32
|
+
week: {
|
|
33
|
+
type: GraphQLInt,
|
|
34
|
+
},
|
|
35
|
+
day: {
|
|
36
|
+
type: GraphQLInt,
|
|
37
|
+
},
|
|
38
|
+
weekday: {
|
|
39
|
+
type: GraphQLInt,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const TimeFunctions = schemaComposer.createObjectTC({
|
|
44
|
+
name: 'time_functions',
|
|
45
|
+
fields: {
|
|
46
|
+
hour: {
|
|
47
|
+
type: GraphQLInt,
|
|
48
|
+
},
|
|
49
|
+
minute: {
|
|
50
|
+
type: GraphQLInt,
|
|
51
|
+
},
|
|
52
|
+
second: {
|
|
53
|
+
type: GraphQLInt,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const DateTimeFunctions = schemaComposer.createObjectTC({
|
|
58
|
+
name: 'datetime_functions',
|
|
59
|
+
fields: {
|
|
60
|
+
...DateFunctions.getFields(),
|
|
61
|
+
...TimeFunctions.getFields(),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
for (const collection of Object.values(schema[action].collections)) {
|
|
65
|
+
if (Object.keys(collection.fields).length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
if (SYSTEM_DENY_LIST.includes(collection.collection))
|
|
68
|
+
continue;
|
|
69
|
+
CollectionTypes[collection.collection] = schemaComposer.createObjectTC({
|
|
70
|
+
name: action === 'read' ? collection.collection : `${action}_${collection.collection}`,
|
|
71
|
+
fields: Object.values(collection.fields).reduce((acc, field) => {
|
|
72
|
+
let type = getGraphQLType(field.type, field.special);
|
|
73
|
+
const fieldIsInconsistent = inconsistentFields[action][collection.collection]?.includes(field.field);
|
|
74
|
+
// GraphQL doesn't differentiate between not-null and has-to-be-submitted. We
|
|
75
|
+
// can't non-null in update, as that would require every not-nullable field to be
|
|
76
|
+
// submitted on updates
|
|
77
|
+
if (field.nullable === false &&
|
|
78
|
+
!field.defaultValue &&
|
|
79
|
+
!GENERATE_SPECIAL.some((flag) => field.special.includes(flag)) &&
|
|
80
|
+
fieldIsInconsistent === false &&
|
|
81
|
+
action !== 'update') {
|
|
82
|
+
type = new GraphQLNonNull(type);
|
|
83
|
+
}
|
|
84
|
+
if (collection.primary === field.field && fieldIsInconsistent === false) {
|
|
85
|
+
// permissions IDs need to be nullable https://github.com/directus/directus/issues/20509
|
|
86
|
+
if (collection.collection === 'directus_permissions') {
|
|
87
|
+
type = GraphQLID;
|
|
88
|
+
}
|
|
89
|
+
else if (!field.defaultValue && !field.special.includes('uuid') && action === 'create') {
|
|
90
|
+
type = new GraphQLNonNull(GraphQLID);
|
|
91
|
+
}
|
|
92
|
+
else if (['create', 'update'].includes(action)) {
|
|
93
|
+
type = GraphQLID;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
type = new GraphQLNonNull(GraphQLID);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
acc[field.field] = {
|
|
100
|
+
type,
|
|
101
|
+
description: field.note,
|
|
102
|
+
resolve: (obj) => {
|
|
103
|
+
return obj[field.field];
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
if (action === 'read') {
|
|
107
|
+
if (field.type === 'date') {
|
|
108
|
+
acc[`${field.field}_func`] = {
|
|
109
|
+
type: DateFunctions,
|
|
110
|
+
resolve: (obj) => {
|
|
111
|
+
const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
112
|
+
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (field.type === 'time') {
|
|
117
|
+
acc[`${field.field}_func`] = {
|
|
118
|
+
type: TimeFunctions,
|
|
119
|
+
resolve: (obj) => {
|
|
120
|
+
const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
121
|
+
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (field.type === 'dateTime' || field.type === 'timestamp') {
|
|
126
|
+
acc[`${field.field}_func`] = {
|
|
127
|
+
type: DateTimeFunctions,
|
|
128
|
+
resolve: (obj) => {
|
|
129
|
+
const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
130
|
+
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (field.type === 'json' || field.type === 'alias') {
|
|
135
|
+
acc[`${field.field}_func`] = {
|
|
136
|
+
type: CountFunctions,
|
|
137
|
+
resolve: (obj) => {
|
|
138
|
+
const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
139
|
+
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return acc;
|
|
145
|
+
}, {}),
|
|
146
|
+
});
|
|
147
|
+
if (scope === 'items') {
|
|
148
|
+
VersionTypes[collection.collection] = CollectionTypes[collection.collection].clone(`version_${collection.collection}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const relation of schema[action].relations) {
|
|
152
|
+
if (relation.related_collection) {
|
|
153
|
+
if (SYSTEM_DENY_LIST.includes(relation.related_collection))
|
|
154
|
+
continue;
|
|
155
|
+
CollectionTypes[relation.collection]?.addFields({
|
|
156
|
+
[relation.field]: {
|
|
157
|
+
type: CollectionTypes[relation.related_collection],
|
|
158
|
+
resolve: (obj, _, __, info) => {
|
|
159
|
+
return obj[info?.path?.key ?? relation.field];
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
VersionTypes[relation.collection]?.addFields({
|
|
164
|
+
[relation.field]: {
|
|
165
|
+
type: GraphQLJSON,
|
|
166
|
+
resolve: (obj, _, __, info) => {
|
|
167
|
+
return obj[info?.path?.key ?? relation.field];
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (relation.meta?.one_field) {
|
|
172
|
+
CollectionTypes[relation.related_collection]?.addFields({
|
|
173
|
+
[relation.meta.one_field]: {
|
|
174
|
+
type: [CollectionTypes[relation.collection]],
|
|
175
|
+
resolve: (obj, _, __, info) => {
|
|
176
|
+
return obj[info?.path?.key ?? relation.meta.one_field];
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
if (scope === 'items') {
|
|
181
|
+
VersionTypes[relation.related_collection]?.addFields({
|
|
182
|
+
[relation.meta.one_field]: {
|
|
183
|
+
type: GraphQLJSON,
|
|
184
|
+
resolve: (obj, _, __, info) => {
|
|
185
|
+
return obj[info?.path?.key ?? relation.meta.one_field];
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (relation.meta?.one_allowed_collections && action === 'read') {
|
|
193
|
+
// NOTE: There are no union input types in GraphQL, so context only applies to Read actions
|
|
194
|
+
CollectionTypes[relation.collection]?.addFields({
|
|
195
|
+
[relation.field]: {
|
|
196
|
+
type: new GraphQLUnionType({
|
|
197
|
+
name: `${relation.collection}_${relation.field}_union`,
|
|
198
|
+
types: relation.meta.one_allowed_collections.map((collection) => CollectionTypes[collection].getType()),
|
|
199
|
+
resolveType(_value, context, info) {
|
|
200
|
+
let path = [];
|
|
201
|
+
let currentPath = info.path;
|
|
202
|
+
while (currentPath.prev) {
|
|
203
|
+
path.push(currentPath.key);
|
|
204
|
+
currentPath = currentPath.prev;
|
|
205
|
+
}
|
|
206
|
+
path = path.reverse().slice(0, -1);
|
|
207
|
+
let parent = context['data'];
|
|
208
|
+
for (const pathPart of path) {
|
|
209
|
+
parent = parent[pathPart];
|
|
210
|
+
}
|
|
211
|
+
const collection = parent[relation.meta.one_collection_field];
|
|
212
|
+
return CollectionTypes[collection].getType().name;
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
resolve: (obj, _, __, info) => {
|
|
216
|
+
return obj[info?.path?.key ?? relation.field];
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { CollectionTypes, VersionTypes };
|
|
223
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SchemaOverview } from '@directus/types';
|
|
2
|
+
import { GraphQLSchema } from 'graphql';
|
|
3
|
+
import type { ObjectTypeComposer } from 'graphql-compose';
|
|
4
|
+
import { type FieldMap } from '../../../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
|
|
5
|
+
import { GraphQLService } from '../index.js';
|
|
6
|
+
export type Schema = {
|
|
7
|
+
read: SchemaOverview;
|
|
8
|
+
create: SchemaOverview;
|
|
9
|
+
update: SchemaOverview;
|
|
10
|
+
delete: SchemaOverview;
|
|
11
|
+
};
|
|
12
|
+
export type InconsistentFields = {
|
|
13
|
+
read: FieldMap;
|
|
14
|
+
create: FieldMap;
|
|
15
|
+
update: FieldMap;
|
|
16
|
+
delete: FieldMap;
|
|
17
|
+
};
|
|
18
|
+
export type CollectionTypes = {
|
|
19
|
+
CreateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
20
|
+
ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
21
|
+
UpdateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
22
|
+
DeleteCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
|
|
26
|
+
*/
|
|
27
|
+
export declare const SYSTEM_DENY_LIST: string[];
|
|
28
|
+
export declare const READ_ONLY: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateSchema(gql: GraphQLService, type?: 'schema' | 'sdl'): Promise<GraphQLSchema | string>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
3
|
+
import { Semaphore } from 'async-mutex';
|
|
4
|
+
import { GraphQLSchema } from 'graphql';
|
|
5
|
+
import { SchemaComposer } from 'graphql-compose';
|
|
6
|
+
import { fetchAllowedFieldMap, } from '../../../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
|
|
7
|
+
import { fetchInconsistentFieldMap } from '../../../permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js';
|
|
8
|
+
import { reduceSchema } from '../../../utils/reduce-schema.js';
|
|
9
|
+
import { GraphQLService } from '../index.js';
|
|
10
|
+
import { injectSystemResolvers } from '../resolvers/system.js';
|
|
11
|
+
import { cache } from '../schema-cache.js';
|
|
12
|
+
import { GraphQLVoid } from '../types/void.js';
|
|
13
|
+
import { sanitizeGraphqlSchema } from '../utils/sanitize-gql-schema.js';
|
|
14
|
+
import { getReadableTypes } from './read.js';
|
|
15
|
+
import { getWritableTypes } from './write.js';
|
|
16
|
+
/**
|
|
17
|
+
* These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
|
|
18
|
+
*/
|
|
19
|
+
export const SYSTEM_DENY_LIST = [
|
|
20
|
+
'directus_collections',
|
|
21
|
+
'directus_fields',
|
|
22
|
+
'directus_relations',
|
|
23
|
+
'directus_migrations',
|
|
24
|
+
'directus_sessions',
|
|
25
|
+
'directus_extensions',
|
|
26
|
+
];
|
|
27
|
+
export const READ_ONLY = ['directus_activity', 'directus_revisions'];
|
|
28
|
+
const env = useEnv();
|
|
29
|
+
const semaphore = new Semaphore(env['GRAPHQL_SCHEMA_GENERATION_MAX_CONCURRENT'] ?? 5);
|
|
30
|
+
/**
|
|
31
|
+
* Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
|
|
32
|
+
*/
|
|
33
|
+
export async function generateSchema(gql, type = 'schema') {
|
|
34
|
+
const key = `${gql.scope}_${type}_${gql.accountability?.role}_${gql.accountability?.user}`;
|
|
35
|
+
const cachedSchema = cache.get(key);
|
|
36
|
+
if (cachedSchema)
|
|
37
|
+
return cachedSchema;
|
|
38
|
+
return semaphore.runExclusive(async () => {
|
|
39
|
+
// Check the cache again after acquiring the lock
|
|
40
|
+
const cachedSchema = cache.get(key);
|
|
41
|
+
if (cachedSchema)
|
|
42
|
+
return cachedSchema;
|
|
43
|
+
const schemaComposer = new SchemaComposer();
|
|
44
|
+
let schema;
|
|
45
|
+
const sanitizedSchema = sanitizeGraphqlSchema(gql.schema);
|
|
46
|
+
if (!gql.accountability || gql.accountability.admin) {
|
|
47
|
+
schema = {
|
|
48
|
+
read: sanitizedSchema,
|
|
49
|
+
create: sanitizedSchema,
|
|
50
|
+
update: sanitizedSchema,
|
|
51
|
+
delete: sanitizedSchema,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
schema = {
|
|
56
|
+
read: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
|
|
57
|
+
accountability: gql.accountability,
|
|
58
|
+
action: 'read',
|
|
59
|
+
}, { schema: gql.schema, knex: gql.knex })),
|
|
60
|
+
create: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
|
|
61
|
+
accountability: gql.accountability,
|
|
62
|
+
action: 'create',
|
|
63
|
+
}, { schema: gql.schema, knex: gql.knex })),
|
|
64
|
+
update: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
|
|
65
|
+
accountability: gql.accountability,
|
|
66
|
+
action: 'update',
|
|
67
|
+
}, { schema: gql.schema, knex: gql.knex })),
|
|
68
|
+
delete: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
|
|
69
|
+
accountability: gql.accountability,
|
|
70
|
+
action: 'delete',
|
|
71
|
+
}, { schema: gql.schema, knex: gql.knex })),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const inconsistentFields = {
|
|
75
|
+
read: await fetchInconsistentFieldMap({
|
|
76
|
+
accountability: gql.accountability,
|
|
77
|
+
action: 'read',
|
|
78
|
+
}, { schema: gql.schema, knex: gql.knex }),
|
|
79
|
+
create: await fetchInconsistentFieldMap({
|
|
80
|
+
accountability: gql.accountability,
|
|
81
|
+
action: 'create',
|
|
82
|
+
}, { schema: gql.schema, knex: gql.knex }),
|
|
83
|
+
update: await fetchInconsistentFieldMap({
|
|
84
|
+
accountability: gql.accountability,
|
|
85
|
+
action: 'update',
|
|
86
|
+
}, { schema: gql.schema, knex: gql.knex }),
|
|
87
|
+
delete: await fetchInconsistentFieldMap({
|
|
88
|
+
accountability: gql.accountability,
|
|
89
|
+
action: 'delete',
|
|
90
|
+
}, { schema: gql.schema, knex: gql.knex }),
|
|
91
|
+
};
|
|
92
|
+
const { ReadCollectionTypes, VersionCollectionTypes } = getReadableTypes(gql, schemaComposer, schema, inconsistentFields);
|
|
93
|
+
const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes(gql, schemaComposer, schema, inconsistentFields, ReadCollectionTypes);
|
|
94
|
+
const CollectionTypes = {
|
|
95
|
+
CreateCollectionTypes,
|
|
96
|
+
ReadCollectionTypes,
|
|
97
|
+
UpdateCollectionTypes,
|
|
98
|
+
DeleteCollectionTypes,
|
|
99
|
+
};
|
|
100
|
+
const scopeFilter = (collection) => {
|
|
101
|
+
if (gql.scope === 'items' && isSystemCollection(collection.collection))
|
|
102
|
+
return false;
|
|
103
|
+
if (gql.scope === 'system') {
|
|
104
|
+
if (isSystemCollection(collection.collection) === false)
|
|
105
|
+
return false;
|
|
106
|
+
if (SYSTEM_DENY_LIST.includes(collection.collection))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
if (gql.scope === 'system') {
|
|
112
|
+
injectSystemResolvers(gql, schemaComposer, CollectionTypes, schema);
|
|
113
|
+
}
|
|
114
|
+
const readableCollections = Object.values(schema.read.collections)
|
|
115
|
+
.filter((collection) => collection.collection in ReadCollectionTypes)
|
|
116
|
+
.filter(scopeFilter);
|
|
117
|
+
if (readableCollections.length > 0) {
|
|
118
|
+
schemaComposer.Query.addFields(readableCollections.reduce((acc, collection) => {
|
|
119
|
+
const collectionName = gql.scope === 'items' ? collection.collection : collection.collection.substring(9);
|
|
120
|
+
acc[collectionName] = ReadCollectionTypes[collection.collection].getResolver(collection.collection);
|
|
121
|
+
if (gql.schema.collections[collection.collection].singleton === false) {
|
|
122
|
+
acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_by_id`);
|
|
123
|
+
acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_aggregated`);
|
|
124
|
+
}
|
|
125
|
+
if (gql.scope === 'items') {
|
|
126
|
+
acc[`${collectionName}_by_version`] = VersionCollectionTypes[collection.collection].getResolver(`${collection.collection}_by_version`);
|
|
127
|
+
}
|
|
128
|
+
return acc;
|
|
129
|
+
}, {}));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
schemaComposer.Query.addFields({
|
|
133
|
+
_empty: {
|
|
134
|
+
type: GraphQLVoid,
|
|
135
|
+
description: "There's no data to query.",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (Object.keys(schema.create.collections).length > 0) {
|
|
140
|
+
schemaComposer.Mutation.addFields(Object.values(schema.create.collections)
|
|
141
|
+
.filter((collection) => collection.collection in CreateCollectionTypes && collection.singleton === false)
|
|
142
|
+
.filter(scopeFilter)
|
|
143
|
+
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
|
|
144
|
+
.reduce((acc, collection) => {
|
|
145
|
+
const collectionName = gql.scope === 'items' ? collection.collection : collection.collection.substring(9);
|
|
146
|
+
acc[`create_${collectionName}_items`] = CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_items`);
|
|
147
|
+
acc[`create_${collectionName}_item`] = CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_item`);
|
|
148
|
+
return acc;
|
|
149
|
+
}, {}));
|
|
150
|
+
}
|
|
151
|
+
if (Object.keys(schema.update.collections).length > 0) {
|
|
152
|
+
schemaComposer.Mutation.addFields(Object.values(schema.update.collections)
|
|
153
|
+
.filter((collection) => collection.collection in UpdateCollectionTypes)
|
|
154
|
+
.filter(scopeFilter)
|
|
155
|
+
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
|
|
156
|
+
.reduce((acc, collection) => {
|
|
157
|
+
const collectionName = gql.scope === 'items' ? collection.collection : collection.collection.substring(9);
|
|
158
|
+
if (collection.singleton) {
|
|
159
|
+
acc[`update_${collectionName}`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
acc[`update_${collectionName}_items`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}_items`);
|
|
163
|
+
acc[`update_${collectionName}_batch`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}_batch`);
|
|
164
|
+
acc[`update_${collectionName}_item`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}_item`);
|
|
165
|
+
}
|
|
166
|
+
return acc;
|
|
167
|
+
}, {}));
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(schema.delete.collections).length > 0) {
|
|
170
|
+
schemaComposer.Mutation.addFields(Object.values(schema.delete.collections)
|
|
171
|
+
.filter((collection) => collection.singleton === false)
|
|
172
|
+
.filter(scopeFilter)
|
|
173
|
+
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
|
|
174
|
+
.reduce((acc, collection) => {
|
|
175
|
+
const collectionName = gql.scope === 'items' ? collection.collection : collection.collection.substring(9);
|
|
176
|
+
acc[`delete_${collectionName}_items`] = DeleteCollectionTypes['many'].getResolver(`delete_${collection.collection}_items`);
|
|
177
|
+
acc[`delete_${collectionName}_item`] = DeleteCollectionTypes['one'].getResolver(`delete_${collection.collection}_item`);
|
|
178
|
+
return acc;
|
|
179
|
+
}, {}));
|
|
180
|
+
}
|
|
181
|
+
if (type === 'sdl') {
|
|
182
|
+
const sdl = schemaComposer.toSDL();
|
|
183
|
+
cache.set(key, sdl);
|
|
184
|
+
return sdl;
|
|
185
|
+
}
|
|
186
|
+
const gqlSchema = schemaComposer.buildSchema();
|
|
187
|
+
cache.set(key, gqlSchema);
|
|
188
|
+
return gqlSchema;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ArgumentNode, GraphQLResolveInfo } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
|
|
4
|
+
* whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
|
|
5
|
+
* arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
|
|
6
|
+
* In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
|
|
7
|
+
* of arguments
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
|
|
3
|
+
* whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
|
|
4
|
+
* arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
|
|
5
|
+
* In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
|
|
6
|
+
* of arguments
|
|
7
|
+
*/
|
|
8
|
+
export function parseArgs(args, variableValues) {
|
|
9
|
+
if (!args || args['length'] === 0)
|
|
10
|
+
return {};
|
|
11
|
+
const parse = (node) => {
|
|
12
|
+
switch (node.kind) {
|
|
13
|
+
case 'Variable':
|
|
14
|
+
return variableValues[node.name.value];
|
|
15
|
+
case 'ListValue':
|
|
16
|
+
return node.values.map(parse);
|
|
17
|
+
case 'ObjectValue':
|
|
18
|
+
return Object.fromEntries(node.fields.map((node) => [node.name.value, parse(node.value)]));
|
|
19
|
+
case 'NullValue':
|
|
20
|
+
return null;
|
|
21
|
+
case 'StringValue':
|
|
22
|
+
return String(node.value);
|
|
23
|
+
case 'IntValue':
|
|
24
|
+
case 'FloatValue':
|
|
25
|
+
return Number(node.value);
|
|
26
|
+
case 'BooleanValue':
|
|
27
|
+
return Boolean(node.value);
|
|
28
|
+
case 'EnumValue':
|
|
29
|
+
default:
|
|
30
|
+
return 'value' in node ? node.value : null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const argsObject = Object.fromEntries(args['map']((arg) => [arg.name.value, parse(arg.value)]));
|
|
34
|
+
return argsObject;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Accountability, Query } from '@directus/types';
|
|
2
|
+
import type { GraphQLResolveInfo, SelectionNode } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
5
|
+
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], accountability?: Accountability | null): Query;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { get, mapKeys, merge, set, uniq } from 'lodash-es';
|
|
2
|
+
import { sanitizeQuery } from '../../../utils/sanitize-query.js';
|
|
3
|
+
import { validateQuery } from '../../../utils/validate-query.js';
|
|
4
|
+
import { replaceFuncs } from '../utils/replace-funcs.js';
|
|
5
|
+
import { parseArgs } from './parse-args.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
8
|
+
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
9
|
+
*/
|
|
10
|
+
export function getQuery(rawQuery, selections, variableValues, accountability) {
|
|
11
|
+
const query = sanitizeQuery(rawQuery, accountability);
|
|
12
|
+
const parseAliases = (selections) => {
|
|
13
|
+
const aliases = {};
|
|
14
|
+
for (const selection of selections) {
|
|
15
|
+
if (selection.kind !== 'Field')
|
|
16
|
+
continue;
|
|
17
|
+
if (selection.alias?.value) {
|
|
18
|
+
aliases[selection.alias.value] = selection.name.value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return aliases;
|
|
22
|
+
};
|
|
23
|
+
const parseFields = (selections, parent) => {
|
|
24
|
+
const fields = [];
|
|
25
|
+
for (let selection of selections) {
|
|
26
|
+
if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true)
|
|
27
|
+
continue;
|
|
28
|
+
selection = selection;
|
|
29
|
+
let current;
|
|
30
|
+
let currentAlias = null;
|
|
31
|
+
// Union type (Many-to-Any)
|
|
32
|
+
if (selection.kind === 'InlineFragment') {
|
|
33
|
+
if (selection.typeCondition.name.value.startsWith('__'))
|
|
34
|
+
continue;
|
|
35
|
+
current = `${parent}:${selection.typeCondition.name.value}`;
|
|
36
|
+
}
|
|
37
|
+
// Any other field type
|
|
38
|
+
else {
|
|
39
|
+
// filter out graphql pointers, like __typename
|
|
40
|
+
if (selection.name.value.startsWith('__'))
|
|
41
|
+
continue;
|
|
42
|
+
current = selection.name.value;
|
|
43
|
+
if (selection.alias) {
|
|
44
|
+
currentAlias = selection.alias.value;
|
|
45
|
+
}
|
|
46
|
+
if (parent) {
|
|
47
|
+
current = `${parent}.${current}`;
|
|
48
|
+
if (currentAlias) {
|
|
49
|
+
currentAlias = `${parent}.${currentAlias}`;
|
|
50
|
+
// add nested aliases into deep query
|
|
51
|
+
if (selection.selectionSet) {
|
|
52
|
+
if (!query.deep)
|
|
53
|
+
query.deep = {};
|
|
54
|
+
set(query.deep, parent, merge({}, get(query.deep, parent), { _alias: { [selection.alias.value]: selection.name.value } }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (selection.selectionSet) {
|
|
60
|
+
let children;
|
|
61
|
+
if (current.endsWith('_func')) {
|
|
62
|
+
children = [];
|
|
63
|
+
const rootField = current.slice(0, -5);
|
|
64
|
+
for (const subSelection of selection.selectionSet.selections) {
|
|
65
|
+
if (subSelection.kind !== 'Field')
|
|
66
|
+
continue;
|
|
67
|
+
if (subSelection.name.value.startsWith('__'))
|
|
68
|
+
continue;
|
|
69
|
+
children.push(`${subSelection.name.value}(${rootField})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
|
|
74
|
+
}
|
|
75
|
+
fields.push(...children);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
fields.push(current);
|
|
79
|
+
}
|
|
80
|
+
if (selection.kind === 'Field' && selection.arguments && selection.arguments.length > 0) {
|
|
81
|
+
if (selection.arguments && selection.arguments.length > 0) {
|
|
82
|
+
if (!query.deep)
|
|
83
|
+
query.deep = {};
|
|
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}`)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return uniq(fields);
|
|
90
|
+
};
|
|
91
|
+
query.alias = parseAliases(selections);
|
|
92
|
+
query.fields = parseFields(selections);
|
|
93
|
+
if (query.filter)
|
|
94
|
+
query.filter = replaceFuncs(query.filter);
|
|
95
|
+
query.deep = replaceFuncs(query.deep);
|
|
96
|
+
validateQuery(query);
|
|
97
|
+
return query;
|
|
98
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaComposer } from 'graphql-compose';
|
|
2
|
+
import { InputTypeComposer, ObjectTypeComposer } from 'graphql-compose';
|
|
3
|
+
import { GraphQLService } from '../index.js';
|
|
4
|
+
import { type InconsistentFields, type Schema } from './index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create readable types and attach resolvers for each. Also prepares full filter argument structures
|
|
7
|
+
*/
|
|
8
|
+
export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): {
|
|
9
|
+
ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
10
|
+
VersionCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
|
|
11
|
+
ReadableCollectionFilterTypes: Record<string, InputTypeComposer<any>>;
|
|
12
|
+
};
|