@strapi/plugin-graphql 4.0.0-next.7 → 4.0.1
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/README.md +1 -1
- package/admin/src/index.js +0 -8
- package/package.json +43 -35
- package/server/bootstrap.js +148 -0
- package/server/config/default-config.js +13 -0
- package/server/config/index.js +7 -0
- package/server/format-graphql-error.js +50 -0
- package/server/services/builders/dynamic-zones.js +97 -0
- package/server/services/builders/entity-meta.js +7 -0
- package/server/services/builders/entity.js +43 -0
- package/server/services/builders/enums.js +24 -0
- package/server/services/builders/filters/content-type.js +93 -0
- package/server/services/builders/filters/index.js +7 -0
- package/server/services/builders/filters/operators/and.js +15 -0
- package/server/services/builders/filters/operators/between.js +15 -0
- package/server/services/builders/filters/operators/contains.js +13 -0
- package/server/services/builders/filters/operators/containsi.js +13 -0
- package/server/services/builders/filters/operators/ends-with.js +13 -0
- package/server/services/builders/filters/operators/eq.js +23 -0
- package/server/services/builders/filters/operators/gt.js +13 -0
- package/server/services/builders/filters/operators/gte.js +13 -0
- package/server/services/builders/filters/operators/in.js +15 -0
- package/server/services/builders/filters/operators/index.js +38 -0
- package/server/services/builders/filters/operators/lt.js +13 -0
- package/server/services/builders/filters/operators/lte.js +13 -0
- package/server/services/builders/filters/operators/ne.js +13 -0
- package/server/services/builders/filters/operators/not-contains.js +13 -0
- package/server/services/builders/filters/operators/not-containsi.js +13 -0
- package/server/services/builders/filters/operators/not-in.js +15 -0
- package/server/services/builders/filters/operators/not-null.js +13 -0
- package/server/services/builders/filters/operators/not.js +19 -0
- package/server/services/builders/filters/operators/null.js +13 -0
- package/server/services/builders/filters/operators/or.js +15 -0
- package/server/services/builders/filters/operators/starts-with.js +13 -0
- package/server/services/builders/generic-morph.js +41 -0
- package/server/services/builders/index.js +92 -0
- package/server/services/builders/input.js +121 -0
- package/server/services/builders/mutations/collection-type.js +191 -0
- package/server/services/builders/mutations/index.js +9 -0
- package/server/services/builders/mutations/single-type.js +141 -0
- package/server/services/builders/queries/collection-type.js +120 -0
- package/server/services/builders/queries/index.js +9 -0
- package/server/services/builders/queries/single-type.js +70 -0
- package/server/services/builders/relation-response-collection.js +35 -0
- package/server/services/builders/resolvers/association.js +85 -0
- package/server/services/builders/resolvers/component.js +18 -0
- package/server/services/builders/resolvers/dynamic-zone.js +9 -0
- package/server/services/builders/resolvers/index.js +18 -0
- package/server/services/builders/resolvers/mutation.js +33 -0
- package/server/services/builders/resolvers/query.js +19 -0
- package/server/services/builders/response-collection.js +43 -0
- package/server/services/builders/response.js +32 -0
- package/server/services/builders/type.js +364 -0
- package/server/services/builders/utils.js +134 -0
- package/server/services/constants.js +149 -0
- package/server/services/content-api/index.js +179 -0
- package/server/services/content-api/policy.js +60 -0
- package/server/services/content-api/register-functions/collection-type.js +72 -0
- package/server/services/content-api/register-functions/component.js +15 -0
- package/server/services/content-api/register-functions/content-type/dynamic-zones.js +36 -0
- package/server/services/content-api/register-functions/content-type/enums.js +33 -0
- package/server/services/content-api/register-functions/content-type/filters.js +15 -0
- package/server/services/content-api/register-functions/content-type/index.js +13 -0
- package/server/services/content-api/register-functions/content-type/inputs.js +21 -0
- package/server/services/content-api/register-functions/index.js +22 -0
- package/server/services/content-api/register-functions/internals.js +13 -0
- package/server/services/content-api/register-functions/polymorphic.js +69 -0
- package/server/services/content-api/register-functions/scalars.js +14 -0
- package/server/services/content-api/register-functions/single-type.js +72 -0
- package/server/services/content-api/wrap-resolvers.js +144 -0
- package/server/services/extension/extension.js +95 -0
- package/server/services/extension/index.js +5 -0
- package/server/services/extension/shadow-crud-manager.js +159 -0
- package/server/services/format/index.js +7 -0
- package/server/services/format/return-types.js +27 -0
- package/server/services/index.js +21 -0
- package/server/services/internals/args/index.js +11 -0
- package/server/services/internals/args/pagination.js +19 -0
- package/server/services/internals/args/publication-state.js +12 -0
- package/server/services/internals/args/sort.js +10 -0
- package/server/services/internals/helpers/get-enabled-scalars.js +15 -0
- package/server/services/internals/helpers/index.js +7 -0
- package/server/services/internals/index.js +13 -0
- package/server/services/internals/scalars/index.js +18 -0
- package/server/services/internals/scalars/time.js +36 -0
- package/server/services/internals/types/error.js +34 -0
- package/server/services/internals/types/filters.js +39 -0
- package/server/services/internals/types/index.js +29 -0
- package/server/services/internals/types/pagination.js +24 -0
- package/server/services/internals/types/publication-state.js +24 -0
- package/server/services/internals/types/response-collection-meta.js +39 -0
- package/server/services/type-registry.js +104 -0
- package/server/services/utils/attributes.js +84 -0
- package/server/services/utils/index.js +11 -0
- package/server/services/utils/mappers/entity-to-response-entity.js +12 -0
- package/server/services/utils/mappers/graphql-filters-to-strapi-query.js +109 -0
- package/server/services/utils/mappers/graphql-scalar-to-operators.js +17 -0
- package/server/services/utils/mappers/index.js +13 -0
- package/server/services/utils/mappers/strapi-scalar-to-graphql-scalar.js +25 -0
- package/server/services/utils/naming.js +287 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +7 -9
- package/admin/src/assets/images/logo.svg +0 -38
- package/config/routes.json +0 -3
- package/config/schema.graphql +0 -1
- package/config/settings.json +0 -12
- package/controllers/GraphQL.js +0 -9
- package/hooks/graphql/defaults.json +0 -5
- package/hooks/graphql/index.js +0 -174
- package/hooks/graphql/load-config.js +0 -42
- package/services/build-aggregation.js +0 -565
- package/services/data-loaders.js +0 -55
- package/services/naming.js +0 -15
- package/services/resolvers-builder.js +0 -204
- package/services/schema-definitions.js +0 -131
- package/services/schema-generator.js +0 -178
- package/services/shadow-crud.js +0 -612
- package/services/type-builder.js +0 -311
- package/services/utils.js +0 -200
- package/types/dynamiczoneScalar.js +0 -40
- package/types/publication-state.js +0 -16
- package/types/time.js +0 -26
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const associationResolvers = require('./association');
|
|
4
|
+
const queriesResolvers = require('./query');
|
|
5
|
+
const mutationsResolvers = require('./mutation');
|
|
6
|
+
const componentResolvers = require('./component');
|
|
7
|
+
const dynamicZoneResolvers = require('./dynamic-zone');
|
|
8
|
+
|
|
9
|
+
module.exports = context => ({
|
|
10
|
+
// Generics
|
|
11
|
+
...associationResolvers(context),
|
|
12
|
+
|
|
13
|
+
// Builders
|
|
14
|
+
...mutationsResolvers(context),
|
|
15
|
+
...queriesResolvers(context),
|
|
16
|
+
...componentResolvers(context),
|
|
17
|
+
...dynamicZoneResolvers(context),
|
|
18
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { pick } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const pickCreateArgs = pick(['params', 'data', 'files']);
|
|
6
|
+
|
|
7
|
+
module.exports = ({ strapi }) => ({
|
|
8
|
+
buildMutationsResolvers({ contentType }) {
|
|
9
|
+
const { uid } = contentType;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
async create(parent, args) {
|
|
13
|
+
// todo[v4]: Might be interesting to generate dynamic yup schema to validate payloads with more complex checks (on top of graphql validation)
|
|
14
|
+
const params = pickCreateArgs(args);
|
|
15
|
+
|
|
16
|
+
// todo[v4]: Sanitize args to only keep params / data / files (or do it in the base resolver)
|
|
17
|
+
return strapi.entityService.create(uid, params);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async update(parent, args) {
|
|
21
|
+
const { id, data } = args;
|
|
22
|
+
|
|
23
|
+
return strapi.entityService.update(uid, id, { data });
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async delete(parent, args) {
|
|
27
|
+
const { id, ...rest } = args;
|
|
28
|
+
|
|
29
|
+
return strapi.entityService.delete(uid, id, rest);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { omit } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = ({ strapi }) => ({
|
|
6
|
+
buildQueriesResolvers({ contentType }) {
|
|
7
|
+
const { uid } = contentType;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
async find(parent, args) {
|
|
11
|
+
return strapi.entityService.findMany(uid, args);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async findOne(parent, args) {
|
|
15
|
+
return strapi.entityService.findOne(uid, args.id, omit('id', args));
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { objectType, nonNull } = require('nexus');
|
|
4
|
+
const { defaultTo, prop, pipe } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
module.exports = ({ strapi }) => {
|
|
7
|
+
const { naming } = strapi.plugin('graphql').service('utils');
|
|
8
|
+
const { RESPONSE_COLLECTION_META_TYPE_NAME } = strapi.plugin('graphql').service('constants');
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
/**
|
|
12
|
+
* Build a type definition for a content API collection response for a given content type
|
|
13
|
+
* @param {object} contentType The content type which will be used to build its content API response definition
|
|
14
|
+
* @return {NexusObjectTypeDef}
|
|
15
|
+
*/
|
|
16
|
+
buildResponseCollectionDefinition(contentType) {
|
|
17
|
+
const name = naming.getEntityResponseCollectionName(contentType);
|
|
18
|
+
const entityName = naming.getEntityName(contentType);
|
|
19
|
+
|
|
20
|
+
return objectType({
|
|
21
|
+
name,
|
|
22
|
+
|
|
23
|
+
definition(t) {
|
|
24
|
+
t.nonNull.list.field('data', {
|
|
25
|
+
type: nonNull(entityName),
|
|
26
|
+
|
|
27
|
+
resolve: pipe(
|
|
28
|
+
prop('nodes'),
|
|
29
|
+
defaultTo([])
|
|
30
|
+
),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
t.nonNull.field('meta', {
|
|
34
|
+
type: RESPONSE_COLLECTION_META_TYPE_NAME,
|
|
35
|
+
|
|
36
|
+
// Pass down the args stored in the source object
|
|
37
|
+
resolve: prop('info'),
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { objectType } = require('nexus');
|
|
4
|
+
const { prop } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
module.exports = ({ strapi }) => {
|
|
7
|
+
const { naming } = strapi.plugin('graphql').service('utils');
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
/**
|
|
11
|
+
* Build a type definition for a content API response for a given content type
|
|
12
|
+
* @param {object} contentType The content type which will be used to build its content API response definition
|
|
13
|
+
* @return {NexusObjectTypeDef}
|
|
14
|
+
*/
|
|
15
|
+
buildResponseDefinition(contentType) {
|
|
16
|
+
const name = naming.getEntityResponseName(contentType);
|
|
17
|
+
const entityName = naming.getEntityName(contentType);
|
|
18
|
+
|
|
19
|
+
return objectType({
|
|
20
|
+
name,
|
|
21
|
+
|
|
22
|
+
definition(t) {
|
|
23
|
+
t.field('data', {
|
|
24
|
+
type: entityName,
|
|
25
|
+
|
|
26
|
+
resolve: prop('value'),
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray, isString, isUndefined, constant } = require('lodash/fp');
|
|
4
|
+
const { objectType } = require('nexus');
|
|
5
|
+
|
|
6
|
+
const { contentTypes } = require('@strapi/utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef TypeBuildersOptions
|
|
10
|
+
*
|
|
11
|
+
* @property {ObjectDefinitionBlock} builder
|
|
12
|
+
* @property {string} attributeName
|
|
13
|
+
* @property {object} attribute
|
|
14
|
+
* @property {object} contentType
|
|
15
|
+
* @property {object} context
|
|
16
|
+
* @property {object} context.strapi
|
|
17
|
+
* @property {object} context.registry
|
|
18
|
+
*/
|
|
19
|
+
module.exports = context => {
|
|
20
|
+
const { strapi } = context;
|
|
21
|
+
|
|
22
|
+
const getGraphQLService = strapi.plugin('graphql').service;
|
|
23
|
+
|
|
24
|
+
const extension = getGraphQLService('extension');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add a scalar attribute to the type definition
|
|
28
|
+
*
|
|
29
|
+
* The attribute is added based on a simple association between a Strapi
|
|
30
|
+
* type and a GraphQL type (the map is defined in `strapiTypeToGraphQLScalar`)
|
|
31
|
+
*
|
|
32
|
+
* @param {TypeBuildersOptions} options
|
|
33
|
+
*/
|
|
34
|
+
const addScalarAttribute = ({ builder, attributeName, attribute }) => {
|
|
35
|
+
const { mappers } = getGraphQLService('utils');
|
|
36
|
+
|
|
37
|
+
const gqlType = mappers.strapiScalarToGraphQLScalar(attribute.type);
|
|
38
|
+
|
|
39
|
+
builder.field(attributeName, { type: gqlType });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add a component attribute to the type definition
|
|
44
|
+
*
|
|
45
|
+
* The attribute is added by fetching the component's type
|
|
46
|
+
* name and using it as the attribute's type
|
|
47
|
+
*
|
|
48
|
+
* @param {TypeBuildersOptions} options
|
|
49
|
+
*/
|
|
50
|
+
const addComponentAttribute = ({ builder, attributeName, contentType, attribute }) => {
|
|
51
|
+
const { naming } = getGraphQLService('utils');
|
|
52
|
+
const { getContentTypeArgs } = getGraphQLService('builders').utils;
|
|
53
|
+
const { buildComponentResolver } = getGraphQLService('builders').get('content-api');
|
|
54
|
+
|
|
55
|
+
const type = naming.getComponentNameFromAttribute(attribute);
|
|
56
|
+
|
|
57
|
+
if (attribute.repeatable) {
|
|
58
|
+
builder = builder.list;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const targetComponent = strapi.getModel(attribute.component);
|
|
62
|
+
|
|
63
|
+
const resolve = buildComponentResolver({
|
|
64
|
+
contentTypeUID: contentType.uid,
|
|
65
|
+
attributeName,
|
|
66
|
+
strapi,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const args = getContentTypeArgs(targetComponent, { multiple: !!attribute.repeatable });
|
|
70
|
+
|
|
71
|
+
builder.field(attributeName, { type, resolve, args });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add a dynamic zone attribute to the type definition
|
|
76
|
+
*
|
|
77
|
+
* The attribute is added by fetching the dynamic zone's
|
|
78
|
+
* type name and using it as the attribute's type
|
|
79
|
+
*
|
|
80
|
+
* @param {TypeBuildersOptions} options
|
|
81
|
+
*/
|
|
82
|
+
const addDynamicZoneAttribute = ({ builder, attributeName, contentType }) => {
|
|
83
|
+
const { naming } = getGraphQLService('utils');
|
|
84
|
+
const { ERROR_CODES } = getGraphQLService('constants');
|
|
85
|
+
const { buildDynamicZoneResolver } = getGraphQLService('builders').get('content-api');
|
|
86
|
+
|
|
87
|
+
const { components } = contentType.attributes[attributeName];
|
|
88
|
+
|
|
89
|
+
const isEmpty = components.length === 0;
|
|
90
|
+
const type = naming.getDynamicZoneName(contentType, attributeName);
|
|
91
|
+
|
|
92
|
+
const resolve = isEmpty
|
|
93
|
+
? // If the dynamic zone don't have any component, then return an error payload
|
|
94
|
+
constant({
|
|
95
|
+
code: ERROR_CODES.emptyDynamicZone,
|
|
96
|
+
message: `This dynamic zone don't have any component attached to it`,
|
|
97
|
+
})
|
|
98
|
+
: // Else, return a classic dynamic-zone resolver
|
|
99
|
+
buildDynamicZoneResolver({
|
|
100
|
+
contentTypeUID: contentType.uid,
|
|
101
|
+
attributeName,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
builder.list.field(attributeName, { type, resolve });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add an enum attribute to the type definition
|
|
109
|
+
*
|
|
110
|
+
* The attribute is added by fetching the enum's type
|
|
111
|
+
* name and using it as the attribute's type
|
|
112
|
+
*
|
|
113
|
+
* @param {TypeBuildersOptions} options
|
|
114
|
+
*/
|
|
115
|
+
const addEnumAttribute = ({ builder, attributeName, contentType }) => {
|
|
116
|
+
const { naming } = getGraphQLService('utils');
|
|
117
|
+
|
|
118
|
+
const type = naming.getEnumName(contentType, attributeName);
|
|
119
|
+
|
|
120
|
+
builder.field(attributeName, { type });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add a media attribute to the type definition
|
|
125
|
+
* @param {TypeBuildersOptions} options
|
|
126
|
+
*/
|
|
127
|
+
const addMediaAttribute = options => {
|
|
128
|
+
const { naming } = getGraphQLService('utils');
|
|
129
|
+
const { getContentTypeArgs } = getGraphQLService('builders').utils;
|
|
130
|
+
const { buildAssociationResolver } = getGraphQLService('builders').get('content-api');
|
|
131
|
+
const extension = getGraphQLService('extension');
|
|
132
|
+
|
|
133
|
+
let { builder } = options;
|
|
134
|
+
const { attributeName, attribute, contentType } = options;
|
|
135
|
+
const fileUID = 'plugin::upload.file';
|
|
136
|
+
|
|
137
|
+
if (extension.shadowCRUD(fileUID).isDisabled()) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fileContentType = strapi.contentTypes[fileUID];
|
|
142
|
+
|
|
143
|
+
const resolve = buildAssociationResolver({
|
|
144
|
+
contentTypeUID: contentType.uid,
|
|
145
|
+
attributeName,
|
|
146
|
+
strapi,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const args = attribute.multiple ? getContentTypeArgs(fileContentType) : undefined;
|
|
150
|
+
const type = attribute.multiple
|
|
151
|
+
? naming.getRelationResponseCollectionName(fileContentType)
|
|
152
|
+
: naming.getEntityResponseName(fileContentType);
|
|
153
|
+
|
|
154
|
+
builder.field(attributeName, { type, resolve, args });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add a polymorphic relational attribute to the type definition
|
|
159
|
+
* @param {TypeBuildersOptions} options
|
|
160
|
+
*/
|
|
161
|
+
const addPolymorphicRelationalAttribute = options => {
|
|
162
|
+
const { GENERIC_MORPH_TYPENAME } = getGraphQLService('constants');
|
|
163
|
+
const { naming } = getGraphQLService('utils');
|
|
164
|
+
const { buildAssociationResolver } = getGraphQLService('builders').get('content-api');
|
|
165
|
+
|
|
166
|
+
let { builder } = options;
|
|
167
|
+
const { attributeName, attribute, contentType } = options;
|
|
168
|
+
|
|
169
|
+
const { target } = attribute;
|
|
170
|
+
const isToManyRelation = attribute.relation.endsWith('Many');
|
|
171
|
+
|
|
172
|
+
if (isToManyRelation) {
|
|
173
|
+
builder = builder.list;
|
|
174
|
+
}
|
|
175
|
+
// todo[v4]: How to handle polymorphic relation w/ entity response collection types?
|
|
176
|
+
// -> Currently return raw polymorphic entities
|
|
177
|
+
|
|
178
|
+
const resolve = buildAssociationResolver({
|
|
179
|
+
contentTypeUID: contentType.uid,
|
|
180
|
+
attributeName,
|
|
181
|
+
strapi,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// If there is no specific target specified, then use the GenericMorph type
|
|
185
|
+
if (isUndefined(target)) {
|
|
186
|
+
builder.field(attributeName, {
|
|
187
|
+
type: GENERIC_MORPH_TYPENAME,
|
|
188
|
+
resolve,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If the target is an array of string, resolve the associated morph type and use it
|
|
193
|
+
else if (isArray(target) && target.every(isString)) {
|
|
194
|
+
const type = naming.getMorphRelationTypeName(contentType, attributeName);
|
|
195
|
+
|
|
196
|
+
builder.field(attributeName, { type, resolve });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add a regular relational attribute to the type definition
|
|
202
|
+
* @param {TypeBuildersOptions} options
|
|
203
|
+
*/
|
|
204
|
+
const addRegularRelationalAttribute = options => {
|
|
205
|
+
const { naming } = getGraphQLService('utils');
|
|
206
|
+
const { getContentTypeArgs } = getGraphQLService('builders').utils;
|
|
207
|
+
const { buildAssociationResolver } = getGraphQLService('builders').get('content-api');
|
|
208
|
+
const extension = getGraphQLService('extension');
|
|
209
|
+
|
|
210
|
+
let { builder } = options;
|
|
211
|
+
const { attributeName, attribute, contentType } = options;
|
|
212
|
+
|
|
213
|
+
if (extension.shadowCRUD(attribute.target).isDisabled()) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const isToManyRelation = attribute.relation.endsWith('Many');
|
|
218
|
+
|
|
219
|
+
const resolve = buildAssociationResolver({
|
|
220
|
+
contentTypeUID: contentType.uid,
|
|
221
|
+
attributeName,
|
|
222
|
+
strapi,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const targetContentType = strapi.getModel(attribute.target);
|
|
226
|
+
|
|
227
|
+
const type = isToManyRelation
|
|
228
|
+
? naming.getRelationResponseCollectionName(targetContentType)
|
|
229
|
+
: naming.getEntityResponseName(targetContentType);
|
|
230
|
+
|
|
231
|
+
const args = isToManyRelation ? getContentTypeArgs(targetContentType) : undefined;
|
|
232
|
+
|
|
233
|
+
const resolverPath = `${naming.getTypeName(contentType)}.${attributeName}`;
|
|
234
|
+
const resolverScope = `${targetContentType.uid}.find`;
|
|
235
|
+
|
|
236
|
+
extension.use({ resolversConfig: { [resolverPath]: { auth: { scope: [resolverScope] } } } });
|
|
237
|
+
|
|
238
|
+
builder.field(attributeName, { type, resolve, args });
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const isNotPrivate = contentType => attributeName => {
|
|
242
|
+
return !contentTypes.isPrivateAttribute(contentType, attributeName);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const isNotDisabled = contentType => attributeName => {
|
|
246
|
+
return extension
|
|
247
|
+
.shadowCRUD(contentType.uid)
|
|
248
|
+
.field(attributeName)
|
|
249
|
+
.hasOutputEnabled();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
/**
|
|
254
|
+
* Create a type definition for a given content type
|
|
255
|
+
* @param contentType - The content type used to created the definition
|
|
256
|
+
* @return {NexusObjectTypeDef}
|
|
257
|
+
*/
|
|
258
|
+
buildTypeDefinition(contentType) {
|
|
259
|
+
const utils = getGraphQLService('utils');
|
|
260
|
+
|
|
261
|
+
const { getComponentName, getTypeName } = utils.naming;
|
|
262
|
+
const {
|
|
263
|
+
isStrapiScalar,
|
|
264
|
+
isComponent,
|
|
265
|
+
isDynamicZone,
|
|
266
|
+
isEnumeration,
|
|
267
|
+
isMedia,
|
|
268
|
+
isMorphRelation,
|
|
269
|
+
isRelation,
|
|
270
|
+
} = utils.attributes;
|
|
271
|
+
|
|
272
|
+
const { attributes, modelType } = contentType;
|
|
273
|
+
|
|
274
|
+
const attributesKey = Object.keys(attributes);
|
|
275
|
+
|
|
276
|
+
const name = (modelType === 'component' ? getComponentName : getTypeName).call(
|
|
277
|
+
null,
|
|
278
|
+
contentType
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return objectType({
|
|
282
|
+
name,
|
|
283
|
+
|
|
284
|
+
definition(t) {
|
|
285
|
+
if (modelType === 'component' && isNotDisabled(contentType)('id')) {
|
|
286
|
+
t.nonNull.id('id');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Attributes
|
|
290
|
+
*
|
|
291
|
+
* Attributes can be of 7 different kind:
|
|
292
|
+
* - Scalar
|
|
293
|
+
* - Component
|
|
294
|
+
* - Dynamic Zone
|
|
295
|
+
* - Enum
|
|
296
|
+
* - Media
|
|
297
|
+
* - Polymorphic Relations
|
|
298
|
+
* - Regular Relations
|
|
299
|
+
*
|
|
300
|
+
* Here, we iterate over each non-private attribute
|
|
301
|
+
* and add it to the type definition based on its type
|
|
302
|
+
*/
|
|
303
|
+
attributesKey
|
|
304
|
+
// Ignore private attributes
|
|
305
|
+
.filter(isNotPrivate(contentType))
|
|
306
|
+
// Ignore disabled fields (from extension service)
|
|
307
|
+
.filter(isNotDisabled(contentType))
|
|
308
|
+
// Add each attribute to the type definition
|
|
309
|
+
.forEach(attributeName => {
|
|
310
|
+
const attribute = attributes[attributeName];
|
|
311
|
+
|
|
312
|
+
// We create a copy of the builder (t) to apply custom
|
|
313
|
+
// rules only on the current attribute (eg: nonNull, list, ...)
|
|
314
|
+
let builder = t;
|
|
315
|
+
|
|
316
|
+
if (attribute.required) {
|
|
317
|
+
builder = builder.nonNull;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @type {TypeBuildersOptions}
|
|
322
|
+
*/
|
|
323
|
+
const options = { builder, attributeName, attribute, contentType, context };
|
|
324
|
+
|
|
325
|
+
// Scalars
|
|
326
|
+
if (isStrapiScalar(attribute)) {
|
|
327
|
+
addScalarAttribute(options);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Components
|
|
331
|
+
else if (isComponent(attribute)) {
|
|
332
|
+
addComponentAttribute(options);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Dynamic Zones
|
|
336
|
+
else if (isDynamicZone(attribute)) {
|
|
337
|
+
addDynamicZoneAttribute(options);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Enums
|
|
341
|
+
else if (isEnumeration(attribute)) {
|
|
342
|
+
addEnumAttribute(options);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Media
|
|
346
|
+
else if (isMedia(attribute)) {
|
|
347
|
+
addMediaAttribute(options);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Polymorphic Relations
|
|
351
|
+
else if (isMorphRelation(attribute)) {
|
|
352
|
+
addPolymorphicRelationalAttribute(options);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Regular Relations
|
|
356
|
+
else if (isRelation(attribute)) {
|
|
357
|
+
addRegularRelationalAttribute(options);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { entries, mapValues, omit } = require('lodash/fp');
|
|
4
|
+
const {
|
|
5
|
+
pagination: { withDefaultPagination },
|
|
6
|
+
contentTypes: { hasDraftAndPublish },
|
|
7
|
+
} = require('@strapi/utils');
|
|
8
|
+
|
|
9
|
+
module.exports = ({ strapi }) => {
|
|
10
|
+
const { service: getService } = strapi.plugin('graphql');
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
/**
|
|
14
|
+
* Get every args for a given content type
|
|
15
|
+
* @param {object} contentType
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {boolean} options.multiple
|
|
18
|
+
* @return {object}
|
|
19
|
+
*/
|
|
20
|
+
getContentTypeArgs(contentType, { multiple = true } = {}) {
|
|
21
|
+
const { naming } = getService('utils');
|
|
22
|
+
const { args } = getService('internals');
|
|
23
|
+
|
|
24
|
+
const { kind, modelType } = contentType;
|
|
25
|
+
|
|
26
|
+
// Components
|
|
27
|
+
if (modelType === 'component') {
|
|
28
|
+
if (!multiple) return {};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
filters: naming.getFiltersInputTypeName(contentType),
|
|
32
|
+
pagination: args.PaginationArg,
|
|
33
|
+
sort: args.SortArg,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Collection Types
|
|
38
|
+
else if (kind === 'collectionType') {
|
|
39
|
+
if (!multiple) {
|
|
40
|
+
return { id: 'ID' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const params = {
|
|
44
|
+
filters: naming.getFiltersInputTypeName(contentType),
|
|
45
|
+
pagination: args.PaginationArg,
|
|
46
|
+
sort: args.SortArg,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (hasDraftAndPublish(contentType)) {
|
|
50
|
+
Object.assign(params, { publicationState: args.PublicationStateArg });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return params;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Single Types
|
|
57
|
+
else if (kind === 'singleType') {
|
|
58
|
+
const params = {};
|
|
59
|
+
|
|
60
|
+
if (hasDraftAndPublish(contentType)) {
|
|
61
|
+
Object.assign(params, { publicationState: args.PublicationStateArg });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return params;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Filter an object entries and keep only those whose value is a unique scalar attribute
|
|
70
|
+
* @param {object} attributes
|
|
71
|
+
* @return {Object<string, object>}
|
|
72
|
+
*/
|
|
73
|
+
getUniqueScalarAttributes(attributes) {
|
|
74
|
+
const { isStrapiScalar } = getService('utils').attributes;
|
|
75
|
+
|
|
76
|
+
const uniqueAttributes = entries(attributes).filter(
|
|
77
|
+
([, attribute]) => isStrapiScalar(attribute) && attribute.unique
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return Object.fromEntries(uniqueAttributes);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Map each value from an attribute to a FiltersInput type name
|
|
85
|
+
* @param {object} attributes - The attributes object to transform
|
|
86
|
+
* @return {Object<string, string>}
|
|
87
|
+
*/
|
|
88
|
+
scalarAttributesToFiltersMap: mapValues(attribute => {
|
|
89
|
+
const { mappers, naming } = getService('utils');
|
|
90
|
+
|
|
91
|
+
const gqlScalar = mappers.strapiScalarToGraphQLScalar(attribute.type);
|
|
92
|
+
|
|
93
|
+
return naming.getScalarFilterInputTypeName(gqlScalar);
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Apply basic transform to GQL args
|
|
98
|
+
*/
|
|
99
|
+
transformArgs(args, { contentType, usePagination = false } = {}) {
|
|
100
|
+
const { mappers } = getService('utils');
|
|
101
|
+
const { config } = strapi.plugin('graphql');
|
|
102
|
+
const { pagination = {}, filters = {} } = args;
|
|
103
|
+
|
|
104
|
+
// Init
|
|
105
|
+
const newArgs = omit(['pagination', 'filters'], args);
|
|
106
|
+
|
|
107
|
+
// Pagination
|
|
108
|
+
if (usePagination) {
|
|
109
|
+
const defaultLimit = config('defaultLimit');
|
|
110
|
+
const maxLimit = config('maxLimit');
|
|
111
|
+
|
|
112
|
+
Object.assign(
|
|
113
|
+
newArgs,
|
|
114
|
+
withDefaultPagination(pagination, {
|
|
115
|
+
maxLimit,
|
|
116
|
+
defaults: {
|
|
117
|
+
offset: { limit: defaultLimit },
|
|
118
|
+
page: { pageSize: defaultLimit },
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Filters
|
|
125
|
+
if (args.filters) {
|
|
126
|
+
Object.assign(newArgs, {
|
|
127
|
+
filters: mappers.graphQLFiltersToStrapiQuery(filters, contentType),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return newArgs;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
};
|