@strapi/plugin-graphql 4.0.0-beta.12 → 4.0.0-beta.16

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@strapi/plugin-graphql",
3
- "version": "4.0.0-beta.12",
3
+ "version": "4.0.0-beta.16",
4
4
  "description": "Adds GraphQL endpoint with default API methods.",
5
5
  "strapi": {
6
6
  "displayName": "GraphQL",
7
7
  "name": "graphql",
8
- "description": "graphql.plugin.description",
8
+ "description": "Adds GraphQL endpoint with default API methods.",
9
9
  "kind": "plugin"
10
10
  },
11
11
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  "@apollo/federation": "^0.28.0",
16
16
  "@graphql-tools/schema": "8.1.2",
17
17
  "@graphql-tools/utils": "^8.0.2",
18
- "@strapi/utils": "4.0.0-beta.12",
18
+ "@strapi/utils": "4.0.0-beta.16",
19
19
  "apollo-server-core": "3.1.2",
20
20
  "apollo-server-koa": "3.1.2",
21
21
  "glob": "^7.1.7",
@@ -29,7 +29,8 @@
29
29
  "koa-compose": "^4.1.0",
30
30
  "lodash": "4.17.21",
31
31
  "nexus": "1.1.0",
32
- "pluralize": "^8.0.0"
32
+ "pluralize": "^8.0.0",
33
+ "subscriptions-transport-ws": "0.9.19"
33
34
  },
34
35
  "devDependencies": {
35
36
  "cross-env": "^7.0.3",
@@ -52,5 +53,5 @@
52
53
  "npm": ">=6.0.0"
53
54
  },
54
55
  "license": "SEE LICENSE IN LICENSE",
55
- "gitHead": "67fee6f3d59df974e8770bb123549f972edda905"
56
+ "gitHead": "71bdfa34637832e8e78a6cf1b57c8c6dbadf133d"
56
57
  }
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const { isEmpty, mergeWith, isArray } = require('lodash/fp');
4
+ const { execute, subscribe } = require('graphql');
5
+ const { SubscriptionServer } = require('subscriptions-transport-ws');
4
6
  const { ApolloServer } = require('apollo-server-koa');
5
7
  const {
6
8
  ApolloServerPluginLandingPageDisabled,
@@ -31,6 +33,8 @@ module.exports = async ({ strapi }) => {
31
33
 
32
34
  const { config } = strapi.plugin('graphql');
33
35
 
36
+ const path = config('endpoint');
37
+
34
38
  const defaultServerConfig = {
35
39
  // Schema
36
40
  schema,
@@ -59,14 +63,29 @@ module.exports = async ({ strapi }) => {
59
63
  ],
60
64
  };
61
65
 
62
- const serverConfig = merge(defaultServerConfig, config('apolloServer', {}));
66
+ const serverConfig = merge(defaultServerConfig, config('apolloServer'));
67
+
68
+ // Handle subscriptions
69
+ if (config('subscriptions')) {
70
+ const subscriptionServer = SubscriptionServer.create(
71
+ { schema, execute, subscribe },
72
+ { server: strapi.server.httpServer, path }
73
+ );
74
+
75
+ serverConfig.plugins.push({
76
+ async serverWillStart() {
77
+ return {
78
+ async drainServer() {
79
+ subscriptionServer.close();
80
+ },
81
+ };
82
+ },
83
+ });
84
+ }
63
85
 
64
86
  // Create a new Apollo server
65
87
  const server = new ApolloServer(serverConfig);
66
88
 
67
- // Link the Apollo server & the Strapi app
68
- const path = config('endpoint', '/graphql');
69
-
70
89
  // Register the upload middleware
71
90
  useUploadMiddleware(strapi, path);
72
91
 
@@ -77,6 +96,7 @@ module.exports = async ({ strapi }) => {
77
96
  strapi.log.error('Failed to start the Apollo server', e.message);
78
97
  }
79
98
 
99
+ // Link the Apollo server & the Strapi app
80
100
  strapi.server.routes([
81
101
  {
82
102
  method: 'ALL',
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ shadowCRUD: true,
5
+
6
+ endpoint: '/graphql',
7
+
8
+ subscriptions: false,
9
+
10
+ maxLimit: -1,
11
+
12
+ apolloServer: {},
13
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const defaultConfig = require('./default-config');
4
+
5
+ module.exports = {
6
+ default: defaultConfig,
7
+ };
@@ -13,7 +13,7 @@ module.exports = ({ strapi }) => {
13
13
  const utils = strapi.plugin('graphql').service('utils');
14
14
  const extension = strapi.plugin('graphql').service('extension');
15
15
 
16
- const { getFiltersInputTypeName } = utils.naming;
16
+ const { getFiltersInputTypeName, getScalarFilterInputTypeName } = utils.naming;
17
17
  const { isStrapiScalar, isRelation } = utils.attributes;
18
18
 
19
19
  const { attributes } = contentType;
@@ -31,6 +31,15 @@ module.exports = ({ strapi }) => {
31
31
  .hasFiltersEnabeld()
32
32
  );
33
33
 
34
+ const isIDFilterEnabled = extension
35
+ .shadowCRUD(contentType.uid)
36
+ .field('id')
37
+ .hasFiltersEnabeld();
38
+ // Add an ID filter to the collection types
39
+ if (contentType.kind === 'collectionType' && isIDFilterEnabled) {
40
+ t.field('id', { type: getScalarFilterInputTypeName('ID') });
41
+ }
42
+
34
43
  // Add every defined attribute
35
44
  for (const [attributeName, attribute] of validAttributes) {
36
45
  // Handle scalars
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const { inputObjectType, nonNull } = require('nexus');
4
+ const {
5
+ contentTypes: { isWritableAttribute },
6
+ } = require('@strapi/utils');
4
7
 
5
8
  module.exports = context => {
6
9
  const { strapi } = context;
@@ -45,9 +48,9 @@ module.exports = context => {
45
48
  .hasInputEnabled();
46
49
  };
47
50
 
48
- const validAttributes = Object.entries(attributes).filter(([attributeName]) =>
49
- isFieldEnabled(attributeName)
50
- );
51
+ const validAttributes = Object.entries(attributes).filter(([attributeName]) => {
52
+ return isWritableAttribute(contentType, attributeName) && isFieldEnabled(attributeName);
53
+ });
51
54
 
52
55
  // Add the ID for the component to enable inplace updates
53
56
  if (modelType === 'component' && isFieldEnabled('id')) {
@@ -4,7 +4,7 @@ const { get } = require('lodash/fp');
4
4
 
5
5
  const utils = require('@strapi/utils');
6
6
 
7
- const { sanitize } = utils;
7
+ const { sanitize, pipeAsync } = utils;
8
8
  const { ApplicationError } = utils.errors;
9
9
 
10
10
  module.exports = ({ strapi }) => {
@@ -65,11 +65,7 @@ module.exports = ({ strapi }) => {
65
65
  const unwrapData = get(attributeName);
66
66
 
67
67
  // Sanitizer definition
68
- const sanitizeMorphAttribute = sanitize.utils.pipeAsync(
69
- wrapData,
70
- sanitizeData,
71
- unwrapData
72
- );
68
+ const sanitizeMorphAttribute = pipeAsync(wrapData, sanitizeData, unwrapData);
73
69
 
74
70
  return sanitizeMorphAttribute(data);
75
71
  }
@@ -5,8 +5,12 @@ module.exports = ({ strapi }) => ({
5
5
  const { transformArgs } = strapi.plugin('graphql').service('builders').utils;
6
6
 
7
7
  return async (parent, args = {}) => {
8
- const contentType = strapi.contentTypes[contentTypeUID];
9
- const transformedArgs = transformArgs(args, { contentType, usePagination: true });
8
+ const contentType = strapi.getModel(contentTypeUID);
9
+
10
+ const { component: componentName } = contentType.attributes[attributeName];
11
+ const component = strapi.getModel(componentName);
12
+
13
+ const transformedArgs = transformArgs(args, { contentType: component, usePagination: true });
10
14
 
11
15
  return strapi.entityService.load(contentTypeUID, parent, attributeName, transformedArgs);
12
16
  };
@@ -66,7 +66,7 @@ module.exports = context => {
66
66
  strapi,
67
67
  });
68
68
 
69
- const args = getContentTypeArgs(targetComponent);
69
+ const args = getContentTypeArgs(targetComponent, { multiple: !!attribute.repeatable });
70
70
 
71
71
  builder.field(attributeName, { type, resolve, args });
72
72
  };
@@ -150,12 +150,6 @@ module.exports = context => {
150
150
  const type = attribute.multiple
151
151
  ? naming.getRelationResponseCollectionName(fileContentType)
152
152
  : naming.getEntityResponseName(fileContentType);
153
- const resolverPath = `${naming.getTypeName(contentType)}.${attributeName}`;
154
- const resolverAuthScope = `${fileUID}.find`;
155
-
156
- extension.use({
157
- resolversConfig: { [resolverPath]: { auth: { scope: [resolverAuthScope] } } },
158
- });
159
153
 
160
154
  builder.field(attributeName, { type, resolve, args });
161
155
  };
@@ -275,10 +269,9 @@ module.exports = context => {
275
269
  isRelation,
276
270
  } = utils.attributes;
277
271
 
278
- const { attributes, modelType, options = {} } = contentType;
272
+ const { attributes, modelType } = contentType;
279
273
 
280
274
  const attributesKey = Object.keys(attributes);
281
- const hasTimestamps = isArray(options.timestamps);
282
275
 
283
276
  const name = (modelType === 'component' ? getComponentName : getTypeName).call(
284
277
  null,
@@ -293,17 +286,7 @@ module.exports = context => {
293
286
  t.nonNull.id('id');
294
287
  }
295
288
 
296
- // 1. Timestamps
297
- // If the content type has timestamps enabled
298
- // then we should add the corresponding attributes in the definition
299
- if (hasTimestamps) {
300
- const [createdAtKey, updatedAtKey] = contentType.options.timestamps;
301
-
302
- t.nonNull.dateTime(createdAtKey);
303
- t.nonNull.dateTime(updatedAtKey);
304
- }
305
-
306
- /** 2. Attributes
289
+ /** Attributes
307
290
  *
308
291
  * Attributes can be of 7 different kind:
309
292
  * - Scalar
@@ -25,6 +25,8 @@ module.exports = ({ strapi }) => {
25
25
 
26
26
  // Components
27
27
  if (modelType === 'component') {
28
+ if (!multiple) return {};
29
+
28
30
  return {
29
31
  filters: naming.getFiltersInputTypeName(contentType),
30
32
  pagination: args.PaginationArg,
@@ -96,6 +98,7 @@ module.exports = ({ strapi }) => {
96
98
  */
97
99
  transformArgs(args, { contentType, usePagination = false } = {}) {
98
100
  const { mappers } = getService('utils');
101
+ const { config } = strapi.plugin('graphql');
99
102
  const { pagination = {}, filters = {} } = args;
100
103
 
101
104
  // Init
@@ -103,8 +106,8 @@ module.exports = ({ strapi }) => {
103
106
 
104
107
  // Pagination
105
108
  if (usePagination) {
106
- const defaultLimit = strapi.plugin('graphql').config('defaultLimit');
107
- const maxLimit = strapi.plugin('graphql').config('maxLimit', -1);
109
+ const defaultLimit = config('defaultLimit');
110
+ const maxLimit = config('maxLimit');
108
111
 
109
112
  Object.assign(
110
113
  newArgs,
@@ -5,6 +5,7 @@ const {
5
5
  makeExecutableSchema,
6
6
  addResolversToSchema,
7
7
  } = require('@graphql-tools/schema');
8
+ const { pruneSchema } = require('@graphql-tools/utils');
8
9
  const { makeSchema } = require('nexus');
9
10
  const { prop, startsWith } = require('lodash/fp');
10
11
 
@@ -37,7 +38,7 @@ module.exports = ({ strapi }) => {
37
38
  let builders;
38
39
 
39
40
  const buildSchema = () => {
40
- const isShadowCRUDEnabled = !!config('shadowCRUD', true);
41
+ const isShadowCRUDEnabled = !!config('shadowCRUD');
41
42
 
42
43
  // Create a new empty type registry
43
44
  registry = getGraphQLService('type-registry').new();
@@ -71,7 +72,11 @@ module.exports = ({ strapi }) => {
71
72
  // Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension
72
73
  const wrappedSchema = wrapResolvers({ schema, strapi, extension });
73
74
 
74
- return wrappedSchema;
75
+ // Prune schema, remove unused types
76
+ // eg: removes registered subscriptions if they're disabled in the config)
77
+ const prunedSchema = pruneSchema(wrappedSchema);
78
+
79
+ return prunedSchema;
75
80
  };
76
81
 
77
82
  const buildSchemas = ({ registry }) => {
@@ -33,8 +33,6 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
33
33
 
34
34
  const typeMap = schema.getTypeMap();
35
35
 
36
- // Iterate over every field from every type within the
37
- // schema's type map and wrap its resolve attribute if needed
38
36
  Object.entries(typeMap).forEach(([type, definition]) => {
39
37
  const isGraphQLObjectType = definition instanceof GraphQLObjectType;
40
38
  const isIgnoredType = introspectionQueries.includes(type);
@@ -82,12 +80,12 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
82
80
  const authConfig = get('auth', resolverConfig);
83
81
  const authContext = get('state.auth', context);
84
82
 
85
- const isMutationOrQuery = ['Mutation', 'Query'].includes(type);
83
+ const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type);
86
84
  const hasConfig = !isNil(authConfig);
87
85
 
88
86
  const isAuthDisabled = authConfig === false;
89
87
 
90
- if ((isMutationOrQuery || hasConfig) && !isAuthDisabled) {
88
+ if ((isValidType || hasConfig) && !isAuthDisabled) {
91
89
  try {
92
90
  await strapi.auth.verify(authContext, authConfig);
93
91
  } catch (error) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { has, propEq, isNil } = require('lodash/fp');
3
+ const { has, propEq, isNil, isDate, isObject } = require('lodash/fp');
4
4
 
5
5
  // todo[v4]: Find a way to get that dynamically
6
6
  const virtualScalarAttributes = ['id'];
@@ -15,7 +15,9 @@ module.exports = ({ strapi }) => {
15
15
  return data.map(recursivelyReplaceScalarOperators);
16
16
  }
17
17
 
18
- if (typeof data !== 'object') {
18
+ // Note: We need to make an exception for date since GraphQL
19
+ // automatically cast date strings to date instances in args
20
+ if (isDate(data) || !isObject(data)) {
19
21
  return data;
20
22
  }
21
23
 
package/strapi-server.js CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  const bootstrap = require('./server/bootstrap');
4
4
  const services = require('./server/services');
5
+ const config = require('./server/config');
5
6
 
6
7
  module.exports = (/* strapi, config */) => {
7
8
  return {
9
+ config,
8
10
  bootstrap,
9
11
  services,
10
12
  };