@strapi/plugin-graphql 4.0.0-beta.2 → 4.0.0-beta.20
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 +23 -23
- package/server/bootstrap.js +28 -4
- 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 +4 -3
- package/server/services/builders/filters/content-type.js +10 -1
- package/server/services/builders/filters/operators/eq.js +5 -1
- package/server/services/builders/input.js +6 -3
- package/server/services/builders/mutations/collection-type.js +23 -2
- package/server/services/builders/mutations/single-type.js +16 -10
- package/server/services/builders/resolvers/association.js +25 -4
- package/server/services/builders/resolvers/component.js +6 -2
- package/server/services/builders/type.js +9 -15
- package/server/services/builders/utils.js +5 -2
- package/server/services/content-api/index.js +29 -18
- package/server/services/content-api/policy.js +11 -10
- package/server/services/content-api/wrap-resolvers.js +4 -6
- package/server/services/internals/scalars/time.js +2 -1
- package/server/services/internals/types/error.js +2 -1
- package/server/services/type-registry.js +2 -1
- package/server/services/utils/mappers/graphql-filters-to-strapi-query.js +4 -2
- package/server/services/utils/mappers/strapi-scalar-to-graphql-scalar.js +2 -1
- package/server/services/utils/naming.js +2 -1
- package/strapi-server.js +2 -0
- package/admin/src/assets/images/logo.svg +0 -38
package/README.md
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
This plugin will add GraphQL functionality to your app.
|
|
4
4
|
By default it will provide you with most of the CRUD methods exposed in the Strapi REST API.
|
|
5
5
|
|
|
6
|
-
To learn more about GraphQL in Strapi [visit documentation](https://strapi.io/
|
|
6
|
+
To learn more about GraphQL in Strapi [visit documentation](https://docs.strapi.io/developer-docs/latest/plugins/graphql.html)
|
package/admin/src/index.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
|
2
2
|
import pluginPkg from '../../package.json';
|
|
3
3
|
import pluginId from './pluginId';
|
|
4
|
-
import pluginLogo from './assets/images/logo.svg';
|
|
5
4
|
|
|
6
|
-
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
|
|
7
|
-
const icon = pluginPkg.strapi.icon;
|
|
8
5
|
const name = pluginPkg.strapi.name;
|
|
9
6
|
|
|
10
7
|
export default {
|
|
11
8
|
register(app) {
|
|
12
9
|
app.registerPlugin({
|
|
13
|
-
description: pluginDescription,
|
|
14
|
-
icon,
|
|
15
10
|
id: pluginId,
|
|
16
|
-
isReady: true,
|
|
17
|
-
isRequired: pluginPkg.strapi.required || false,
|
|
18
11
|
name,
|
|
19
|
-
pluginLogo,
|
|
20
12
|
});
|
|
21
13
|
},
|
|
22
14
|
bootstrap() {},
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/plugin-graphql",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.20",
|
|
4
4
|
"description": "Adds GraphQL endpoint with default API methods.",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"name": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"kind": "plugin"
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Strapi Solutions SAS",
|
|
8
|
+
"email": "hi@strapi.io",
|
|
9
|
+
"url": "https://strapi.io"
|
|
11
10
|
},
|
|
11
|
+
"maintainers": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Strapi Solutions SAS",
|
|
14
|
+
"email": "hi@strapi.io",
|
|
15
|
+
"url": "https://strapi.io"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
12
18
|
"scripts": {
|
|
13
19
|
"test": "echo \"no tests yet\""
|
|
14
20
|
},
|
|
@@ -16,7 +22,7 @@
|
|
|
16
22
|
"@apollo/federation": "^0.28.0",
|
|
17
23
|
"@graphql-tools/schema": "8.1.2",
|
|
18
24
|
"@graphql-tools/utils": "^8.0.2",
|
|
19
|
-
"@strapi/utils": "4.0.0-beta.
|
|
25
|
+
"@strapi/utils": "4.0.0-beta.20",
|
|
20
26
|
"apollo-server-core": "3.1.2",
|
|
21
27
|
"apollo-server-koa": "3.1.2",
|
|
22
28
|
"glob": "^7.1.7",
|
|
@@ -30,28 +36,22 @@
|
|
|
30
36
|
"koa-compose": "^4.1.0",
|
|
31
37
|
"lodash": "4.17.21",
|
|
32
38
|
"nexus": "1.1.0",
|
|
33
|
-
"pluralize": "^8.0.0"
|
|
39
|
+
"pluralize": "^8.0.0",
|
|
40
|
+
"subscriptions-transport-ws": "0.9.19"
|
|
34
41
|
},
|
|
35
42
|
"devDependencies": {
|
|
36
43
|
"cross-env": "^7.0.3",
|
|
37
44
|
"koa": "^2.13.1"
|
|
38
45
|
},
|
|
39
|
-
"author": {
|
|
40
|
-
"name": "A Strapi developer",
|
|
41
|
-
"email": "",
|
|
42
|
-
"url": ""
|
|
43
|
-
},
|
|
44
|
-
"maintainers": [
|
|
45
|
-
{
|
|
46
|
-
"name": "A Strapi developer",
|
|
47
|
-
"email": "",
|
|
48
|
-
"url": ""
|
|
49
|
-
}
|
|
50
|
-
],
|
|
51
46
|
"engines": {
|
|
52
47
|
"node": ">=12.x.x <=16.x.x",
|
|
53
48
|
"npm": ">=6.0.0"
|
|
54
49
|
},
|
|
55
|
-
"
|
|
56
|
-
|
|
50
|
+
"strapi": {
|
|
51
|
+
"displayName": "GraphQL",
|
|
52
|
+
"name": "graphql",
|
|
53
|
+
"description": "Adds GraphQL endpoint with default API methods.",
|
|
54
|
+
"kind": "plugin"
|
|
55
|
+
},
|
|
56
|
+
"gitHead": "b4993dab9f6dbc583709167f459b6f00e0b4baa6"
|
|
57
57
|
}
|
package/server/bootstrap.js
CHANGED
|
@@ -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,
|
|
@@ -8,6 +10,7 @@ const {
|
|
|
8
10
|
} = require('apollo-server-core');
|
|
9
11
|
const depthLimit = require('graphql-depth-limit');
|
|
10
12
|
const { graphqlUploadKoa } = require('graphql-upload');
|
|
13
|
+
const formatGraphqlError = require('./format-graphql-error');
|
|
11
14
|
|
|
12
15
|
const merge = mergeWith((a, b) => {
|
|
13
16
|
if (isArray(a) && isArray(b)) {
|
|
@@ -30,6 +33,8 @@ module.exports = async ({ strapi }) => {
|
|
|
30
33
|
|
|
31
34
|
const { config } = strapi.plugin('graphql');
|
|
32
35
|
|
|
36
|
+
const path = config('endpoint');
|
|
37
|
+
|
|
33
38
|
const defaultServerConfig = {
|
|
34
39
|
// Schema
|
|
35
40
|
schema,
|
|
@@ -43,6 +48,9 @@ module.exports = async ({ strapi }) => {
|
|
|
43
48
|
// Validation
|
|
44
49
|
validationRules: [depthLimit(config('depthLimit'))],
|
|
45
50
|
|
|
51
|
+
// Errors
|
|
52
|
+
formatError: formatGraphqlError,
|
|
53
|
+
|
|
46
54
|
// Misc
|
|
47
55
|
cors: false,
|
|
48
56
|
uploads: false,
|
|
@@ -55,14 +63,29 @@ module.exports = async ({ strapi }) => {
|
|
|
55
63
|
],
|
|
56
64
|
};
|
|
57
65
|
|
|
58
|
-
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
|
+
}
|
|
59
85
|
|
|
60
86
|
// Create a new Apollo server
|
|
61
87
|
const server = new ApolloServer(serverConfig);
|
|
62
88
|
|
|
63
|
-
// Link the Apollo server & the Strapi app
|
|
64
|
-
const path = config('endpoint', '/graphql');
|
|
65
|
-
|
|
66
89
|
// Register the upload middleware
|
|
67
90
|
useUploadMiddleware(strapi, path);
|
|
68
91
|
|
|
@@ -73,6 +96,7 @@ module.exports = async ({ strapi }) => {
|
|
|
73
96
|
strapi.log.error('Failed to start the Apollo server', e.message);
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
// Link the Apollo server & the Strapi app
|
|
76
100
|
strapi.server.routes([
|
|
77
101
|
{
|
|
78
102
|
method: 'ALL',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { toUpper, snakeCase, pick, isEmpty } = require('lodash/fp');
|
|
4
|
+
const {
|
|
5
|
+
HttpError,
|
|
6
|
+
ForbiddenError,
|
|
7
|
+
UnauthorizedError,
|
|
8
|
+
ApplicationError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
} = require('@strapi/utils').errors;
|
|
11
|
+
const {
|
|
12
|
+
ApolloError,
|
|
13
|
+
UserInputError: ApolloUserInputError,
|
|
14
|
+
ForbiddenError: ApolloForbiddenError,
|
|
15
|
+
} = require('apollo-server-koa');
|
|
16
|
+
const { GraphQLError } = require('graphql');
|
|
17
|
+
|
|
18
|
+
const formatToCode = name => `STRAPI_${toUpper(snakeCase(name))}`;
|
|
19
|
+
const formatErrorToExtension = error => ({ error: pick(['name', 'message', 'details'])(error) });
|
|
20
|
+
|
|
21
|
+
const formatGraphqlError = error => {
|
|
22
|
+
const { originalError } = error;
|
|
23
|
+
|
|
24
|
+
if (isEmpty(originalError)) {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (originalError instanceof ForbiddenError || originalError instanceof UnauthorizedError) {
|
|
29
|
+
return new ApolloForbiddenError(originalError.message, formatErrorToExtension(originalError));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (originalError instanceof ValidationError) {
|
|
33
|
+
return new ApolloUserInputError(originalError.message, formatErrorToExtension(originalError));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (originalError instanceof ApplicationError || originalError instanceof HttpError) {
|
|
37
|
+
const name = formatToCode(originalError.name);
|
|
38
|
+
return new ApolloError(originalError.message, name, formatErrorToExtension(originalError));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (originalError instanceof ApolloError || originalError instanceof GraphQLError) {
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Internal server error
|
|
46
|
+
strapi.log.error(originalError);
|
|
47
|
+
return ApolloError('Internal Server Error', 'INTERNAL_SERVER_ERROR');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = formatGraphqlError;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { Kind, valueFromASTUntyped
|
|
3
|
+
const { Kind, valueFromASTUntyped } = require('graphql');
|
|
4
4
|
const { omit } = require('lodash/fp');
|
|
5
5
|
const { unionType, scalarType } = require('nexus');
|
|
6
|
+
const { ApplicationError } = require('@strapi/utils');
|
|
6
7
|
|
|
7
8
|
module.exports = ({ strapi }) => {
|
|
8
9
|
const buildTypeDefinition = (name, components) => {
|
|
@@ -13,7 +14,7 @@ module.exports = ({ strapi }) => {
|
|
|
13
14
|
const component = strapi.components[componentUID];
|
|
14
15
|
|
|
15
16
|
if (!component) {
|
|
16
|
-
throw new
|
|
17
|
+
throw new ApplicationError(
|
|
17
18
|
`Trying to create a dynamic zone type with an unknown component: "${componentUID}"`
|
|
18
19
|
);
|
|
19
20
|
}
|
|
@@ -45,7 +46,7 @@ module.exports = ({ strapi }) => {
|
|
|
45
46
|
);
|
|
46
47
|
|
|
47
48
|
if (!component) {
|
|
48
|
-
throw new
|
|
49
|
+
throw new ApplicationError(
|
|
49
50
|
`Component not found. expected one of: ${components
|
|
50
51
|
.map(uid => strapi.components[uid].globalId)
|
|
51
52
|
.join(', ')}`
|
|
@@ -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,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { ValidationError } = require('@strapi/utils').errors;
|
|
4
|
+
|
|
3
5
|
const EQ_FIELD_NAME = 'eq';
|
|
4
6
|
|
|
5
7
|
module.exports = ({ strapi }) => ({
|
|
@@ -11,7 +13,9 @@ module.exports = ({ strapi }) => ({
|
|
|
11
13
|
const { GRAPHQL_SCALARS } = strapi.plugin('graphql').service('constants');
|
|
12
14
|
|
|
13
15
|
if (!GRAPHQL_SCALARS.includes(type)) {
|
|
14
|
-
throw new
|
|
16
|
+
throw new ValidationError(
|
|
17
|
+
`Can't use "${EQ_FIELD_NAME}" operator. "${type}" is not a valid scalar`
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
t.field(EQ_FIELD_NAME, { type });
|
|
@@ -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')) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { extendType, nonNull } = require('nexus');
|
|
4
|
+
const { sanitize } = require('@strapi/utils');
|
|
4
5
|
|
|
5
6
|
module.exports = ({ strapi }) => {
|
|
6
7
|
const { service: getService } = strapi.plugin('graphql');
|
|
@@ -31,9 +32,19 @@ module.exports = ({ strapi }) => {
|
|
|
31
32
|
data: nonNull(getContentTypeInputName(contentType)),
|
|
32
33
|
},
|
|
33
34
|
|
|
34
|
-
async resolve(parent, args) {
|
|
35
|
+
async resolve(parent, args, context) {
|
|
36
|
+
const { auth } = context.state;
|
|
35
37
|
const transformedArgs = transformArgs(args, { contentType });
|
|
36
38
|
|
|
39
|
+
// Sanitize input data
|
|
40
|
+
const sanitizedInputData = await sanitize.contentAPI.input(
|
|
41
|
+
transformedArgs.data,
|
|
42
|
+
contentType,
|
|
43
|
+
{ auth }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
Object.assign(transformedArgs, { data: sanitizedInputData });
|
|
47
|
+
|
|
37
48
|
const { create } = getService('builders')
|
|
38
49
|
.get('content-api')
|
|
39
50
|
.buildMutationsResolvers({ contentType });
|
|
@@ -68,9 +79,19 @@ module.exports = ({ strapi }) => {
|
|
|
68
79
|
data: nonNull(getContentTypeInputName(contentType)),
|
|
69
80
|
},
|
|
70
81
|
|
|
71
|
-
async resolve(parent, args) {
|
|
82
|
+
async resolve(parent, args, context) {
|
|
83
|
+
const { auth } = context.state;
|
|
72
84
|
const transformedArgs = transformArgs(args, { contentType });
|
|
73
85
|
|
|
86
|
+
// Sanitize input data
|
|
87
|
+
const sanitizedInputData = await sanitize.contentAPI.input(
|
|
88
|
+
transformedArgs.data,
|
|
89
|
+
contentType,
|
|
90
|
+
{ auth }
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
Object.assign(transformedArgs, { data: sanitizedInputData });
|
|
94
|
+
|
|
74
95
|
const { update } = getService('builders')
|
|
75
96
|
.get('content-api')
|
|
76
97
|
.buildMutationsResolvers({ contentType });
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { extendType, nonNull } = require('nexus');
|
|
4
4
|
const { omit, isNil } = require('lodash/fp');
|
|
5
|
-
const { getNonWritableAttributes } = require('@strapi/utils').contentTypes;
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const utils = require('@strapi/utils');
|
|
7
|
+
|
|
8
|
+
const { sanitize } = utils;
|
|
9
|
+
const { NotFoundError } = utils.errors;
|
|
8
10
|
|
|
9
11
|
module.exports = ({ strapi }) => {
|
|
10
12
|
const { service: getService } = strapi.plugin('graphql');
|
|
@@ -34,11 +36,18 @@ module.exports = ({ strapi }) => {
|
|
|
34
36
|
data: nonNull(getContentTypeInputName(contentType)),
|
|
35
37
|
},
|
|
36
38
|
|
|
37
|
-
async resolve(parent, args) {
|
|
39
|
+
async resolve(parent, args, context) {
|
|
40
|
+
const { auth } = context.state;
|
|
38
41
|
const transformedArgs = transformArgs(args, { contentType });
|
|
39
42
|
|
|
40
43
|
// Sanitize input data
|
|
41
|
-
|
|
44
|
+
const sanitizedInputData = await sanitize.contentAPI.input(
|
|
45
|
+
transformedArgs.data,
|
|
46
|
+
contentType,
|
|
47
|
+
{ auth }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Object.assign(transformedArgs, { data: sanitizedInputData });
|
|
42
51
|
|
|
43
52
|
const { create, update } = getService('builders')
|
|
44
53
|
.get('content-api')
|
|
@@ -71,20 +80,17 @@ module.exports = ({ strapi }) => {
|
|
|
71
80
|
async resolve(parent, args) {
|
|
72
81
|
const transformedArgs = transformArgs(args, { contentType });
|
|
73
82
|
|
|
74
|
-
Object.assign(transformedArgs, { data: sanitizeInput(contentType, transformedArgs.data) });
|
|
75
|
-
|
|
76
83
|
const { delete: deleteResolver } = getService('builders')
|
|
77
84
|
.get('content-api')
|
|
78
85
|
.buildMutationsResolvers({ contentType });
|
|
79
86
|
|
|
80
|
-
const
|
|
81
|
-
const entity = await strapi.entityService.findMany(uid, { params });
|
|
87
|
+
const entity = await strapi.entityService.findMany(uid, { params: transformedArgs });
|
|
82
88
|
|
|
83
89
|
if (!entity) {
|
|
84
|
-
throw new
|
|
90
|
+
throw new NotFoundError('Entity not found');
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
const value = await deleteResolver(parent, { id: entity.id, params });
|
|
93
|
+
const value = await deleteResolver(parent, { id: entity.id, params: transformedArgs });
|
|
88
94
|
|
|
89
95
|
return toEntityResponse(value, { args: transformedArgs, resourceUID: uid });
|
|
90
96
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { get } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const utils = require('@strapi/utils');
|
|
6
|
+
|
|
7
|
+
const { sanitize, pipeAsync } = utils;
|
|
8
|
+
const { ApplicationError } = utils.errors;
|
|
9
|
+
|
|
3
10
|
module.exports = ({ strapi }) => {
|
|
4
11
|
const { service: getGraphQLService } = strapi.plugin('graphql');
|
|
5
12
|
|
|
@@ -13,7 +20,7 @@ module.exports = ({ strapi }) => {
|
|
|
13
20
|
const attribute = contentType.attributes[attributeName];
|
|
14
21
|
|
|
15
22
|
if (!attribute) {
|
|
16
|
-
throw new
|
|
23
|
+
throw new ApplicationError(
|
|
17
24
|
`Failed to build an association resolver for ${contentTypeUID}::${attributeName}`
|
|
18
25
|
);
|
|
19
26
|
}
|
|
@@ -26,7 +33,9 @@ module.exports = ({ strapi }) => {
|
|
|
26
33
|
|
|
27
34
|
const targetContentType = strapi.getModel(targetUID);
|
|
28
35
|
|
|
29
|
-
return async (parent, args = {}) => {
|
|
36
|
+
return async (parent, args = {}, context) => {
|
|
37
|
+
const { auth } = context.state;
|
|
38
|
+
|
|
30
39
|
const transformedArgs = transformArgs(args, {
|
|
31
40
|
contentType: targetContentType,
|
|
32
41
|
usePagination: true,
|
|
@@ -44,9 +53,21 @@ module.exports = ({ strapi }) => {
|
|
|
44
53
|
resourceUID: targetUID,
|
|
45
54
|
};
|
|
46
55
|
|
|
47
|
-
// If this a polymorphic association, it returns the raw data
|
|
56
|
+
// If this a polymorphic association, it sanitizes & returns the raw data
|
|
57
|
+
// Note: The value needs to be wrapped in a fake object that represents its parent
|
|
58
|
+
// so that the sanitize util can work properly.
|
|
48
59
|
if (isMorphAttribute) {
|
|
49
|
-
|
|
60
|
+
// Helpers used for the data cleanup
|
|
61
|
+
const wrapData = dataToWrap => ({ [attributeName]: dataToWrap });
|
|
62
|
+
const sanitizeData = dataToSanitize => {
|
|
63
|
+
return sanitize.contentAPI.output(dataToSanitize, contentType, { auth });
|
|
64
|
+
};
|
|
65
|
+
const unwrapData = get(attributeName);
|
|
66
|
+
|
|
67
|
+
// Sanitizer definition
|
|
68
|
+
const sanitizeMorphAttribute = pipeAsync(wrapData, sanitizeData, unwrapData);
|
|
69
|
+
|
|
70
|
+
return sanitizeMorphAttribute(data);
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
// If this is a to-many relation, it returns an object that
|
|
@@ -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.
|
|
9
|
-
|
|
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
|
};
|
|
@@ -230,6 +230,11 @@ module.exports = context => {
|
|
|
230
230
|
|
|
231
231
|
const args = isToManyRelation ? getContentTypeArgs(targetContentType) : undefined;
|
|
232
232
|
|
|
233
|
+
const resolverPath = `${naming.getTypeName(contentType)}.${attributeName}`;
|
|
234
|
+
const resolverScope = `${targetContentType.uid}.find`;
|
|
235
|
+
|
|
236
|
+
extension.use({ resolversConfig: { [resolverPath]: { auth: { scope: [resolverScope] } } } });
|
|
237
|
+
|
|
233
238
|
builder.field(attributeName, { type, resolve, args });
|
|
234
239
|
};
|
|
235
240
|
|
|
@@ -264,10 +269,9 @@ module.exports = context => {
|
|
|
264
269
|
isRelation,
|
|
265
270
|
} = utils.attributes;
|
|
266
271
|
|
|
267
|
-
const { attributes, modelType
|
|
272
|
+
const { attributes, modelType } = contentType;
|
|
268
273
|
|
|
269
274
|
const attributesKey = Object.keys(attributes);
|
|
270
|
-
const hasTimestamps = isArray(options.timestamps);
|
|
271
275
|
|
|
272
276
|
const name = (modelType === 'component' ? getComponentName : getTypeName).call(
|
|
273
277
|
null,
|
|
@@ -282,17 +286,7 @@ module.exports = context => {
|
|
|
282
286
|
t.nonNull.id('id');
|
|
283
287
|
}
|
|
284
288
|
|
|
285
|
-
|
|
286
|
-
// If the content type has timestamps enabled
|
|
287
|
-
// then we should add the corresponding attributes in the definition
|
|
288
|
-
if (hasTimestamps) {
|
|
289
|
-
const [createdAtKey, updatedAtKey] = contentType.options.timestamps;
|
|
290
|
-
|
|
291
|
-
t.nonNull.dateTime(createdAtKey);
|
|
292
|
-
t.nonNull.dateTime(updatedAtKey);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** 2. Attributes
|
|
289
|
+
/** Attributes
|
|
296
290
|
*
|
|
297
291
|
* Attributes can be of 7 different kind:
|
|
298
292
|
* - Scalar
|
|
@@ -359,7 +353,7 @@ module.exports = context => {
|
|
|
359
353
|
}
|
|
360
354
|
|
|
361
355
|
// Regular Relations
|
|
362
|
-
else if (isRelation(attribute)
|
|
356
|
+
else if (isRelation(attribute)) {
|
|
363
357
|
addRegularRelationalAttribute(options);
|
|
364
358
|
}
|
|
365
359
|
});
|
|
@@ -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 =
|
|
107
|
-
const maxLimit =
|
|
109
|
+
const defaultLimit = config('defaultLimit');
|
|
110
|
+
const maxLimit = config('maxLimit');
|
|
108
111
|
|
|
109
112
|
Object.assign(
|
|
110
113
|
newArgs,
|
|
@@ -5,8 +5,9 @@ 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
|
-
const {
|
|
10
|
+
const { prop, startsWith } = require('lodash/fp');
|
|
10
11
|
|
|
11
12
|
const { wrapResolvers } = require('./wrap-resolvers');
|
|
12
13
|
const {
|
|
@@ -29,6 +30,7 @@ module.exports = ({ strapi }) => {
|
|
|
29
30
|
const { config } = strapi.plugin('graphql');
|
|
30
31
|
|
|
31
32
|
const { KINDS, GENERIC_MORPH_TYPENAME } = getGraphQLService('constants');
|
|
33
|
+
const extensionService = getGraphQLService('extension');
|
|
32
34
|
|
|
33
35
|
// Type Registry
|
|
34
36
|
let registry;
|
|
@@ -36,9 +38,7 @@ module.exports = ({ strapi }) => {
|
|
|
36
38
|
let builders;
|
|
37
39
|
|
|
38
40
|
const buildSchema = () => {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const isShadowCRUDEnabled = !!config('shadowCRUD', true);
|
|
41
|
+
const isShadowCRUDEnabled = !!config('shadowCRUD');
|
|
42
42
|
|
|
43
43
|
// Create a new empty type registry
|
|
44
44
|
registry = getGraphQLService('type-registry').new();
|
|
@@ -54,24 +54,35 @@ module.exports = ({ strapi }) => {
|
|
|
54
54
|
shadowCRUD();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// Build a collection of schema based on the type registry (& temporary generated extension)
|
|
58
|
+
const schemas = buildSchemas({ registry });
|
|
59
|
+
|
|
60
|
+
// Merge every created schema into a single one
|
|
61
|
+
const mergedSchema = mergeSchemas({ schemas });
|
|
62
|
+
|
|
63
|
+
// Generate the extension configuration for the content API.
|
|
64
|
+
// This extension instance needs to be generated after the Nexus schema's
|
|
65
|
+
// generation, so that configurations created during types definitions
|
|
66
|
+
// can be registered before being used in the wrap resolvers operation
|
|
58
67
|
const extension = extensionService.generate({ typeRegistry: registry });
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// Add the extension's resolvers to the final schema
|
|
70
|
+
const schema = addResolversToSchema(mergedSchema, extension.resolvers);
|
|
71
|
+
|
|
72
|
+
// Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension
|
|
73
|
+
const wrappedSchema = wrapResolvers({ schema, strapi, extension });
|
|
74
|
+
|
|
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;
|
|
71
80
|
};
|
|
72
81
|
|
|
73
|
-
const buildSchemas = ({ registry
|
|
74
|
-
|
|
82
|
+
const buildSchemas = ({ registry }) => {
|
|
83
|
+
// Here we extract types, plugins & typeDefs from a temporary generated
|
|
84
|
+
// extension since there won't be any addition allowed after schemas generation
|
|
85
|
+
const { types, plugins, typeDefs = [] } = extensionService.generate({ typeRegistry: registry });
|
|
75
86
|
|
|
76
87
|
// Create a new Nexus schema (shadow CRUD) & add it to the schemas collection
|
|
77
88
|
const nexusSchema = makeSchema({
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { propOr } = require('lodash/fp');
|
|
4
4
|
const { policy: policyUtils } = require('@strapi/utils');
|
|
5
|
+
const { ForbiddenError } = require('@strapi/utils').errors;
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
return async (resolve, ...rest) => {
|
|
8
|
-
const resolverPolicies = getOr([], 'policies', resolverConfig);
|
|
7
|
+
const getPoliciesConfig = propOr([], 'policies');
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const createPoliciesMiddleware = (resolverConfig, { strapi }) => {
|
|
10
|
+
const resolverPolicies = getPoliciesConfig(resolverConfig);
|
|
11
|
+
const policies = policyUtils.resolve(resolverPolicies);
|
|
12
12
|
|
|
13
|
+
return async (resolve, ...rest) => {
|
|
13
14
|
// Create a graphql policy context
|
|
14
15
|
const context = createGraphQLPolicyContext(...rest);
|
|
15
16
|
|
|
16
17
|
// Run policies & throw an error if one of them fails
|
|
17
|
-
for (const
|
|
18
|
-
const result = await
|
|
18
|
+
for (const { handler, config } of policies) {
|
|
19
|
+
const result = await handler(context, config, { strapi });
|
|
19
20
|
|
|
20
|
-
if (!result) {
|
|
21
|
-
throw new
|
|
21
|
+
if (![true, undefined].includes(result)) {
|
|
22
|
+
throw new ForbiddenError('Policies failed.');
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { get, getOr, isFunction, first, isNil } = require('lodash/fp');
|
|
4
4
|
|
|
5
5
|
const { GraphQLObjectType } = require('graphql');
|
|
6
|
+
const { ForbiddenError } = require('@strapi/utils').errors;
|
|
6
7
|
const { createPoliciesMiddleware } = require('./policy');
|
|
7
8
|
|
|
8
9
|
const introspectionQueries = [
|
|
@@ -32,8 +33,6 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
|
|
|
32
33
|
|
|
33
34
|
const typeMap = schema.getTypeMap();
|
|
34
35
|
|
|
35
|
-
// Iterate over every field from every type within the
|
|
36
|
-
// schema's type map and wrap its resolve attribute if needed
|
|
37
36
|
Object.entries(typeMap).forEach(([type, definition]) => {
|
|
38
37
|
const isGraphQLObjectType = definition instanceof GraphQLObjectType;
|
|
39
38
|
const isIgnoredType = introspectionQueries.includes(type);
|
|
@@ -81,17 +80,16 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
|
|
|
81
80
|
const authConfig = get('auth', resolverConfig);
|
|
82
81
|
const authContext = get('state.auth', context);
|
|
83
82
|
|
|
84
|
-
const
|
|
83
|
+
const isValidType = ['Mutation', 'Query', 'Subscription'].includes(type);
|
|
85
84
|
const hasConfig = !isNil(authConfig);
|
|
86
85
|
|
|
87
86
|
const isAuthDisabled = authConfig === false;
|
|
88
87
|
|
|
89
|
-
if ((
|
|
88
|
+
if ((isValidType || hasConfig) && !isAuthDisabled) {
|
|
90
89
|
try {
|
|
91
90
|
await strapi.auth.verify(authContext, authConfig);
|
|
92
91
|
} catch (error) {
|
|
93
|
-
|
|
94
|
-
throw new Error('Forbidden access');
|
|
92
|
+
throw new ForbiddenError();
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { GraphQLScalarType } = require('graphql');
|
|
4
4
|
const { Kind } = require('graphql');
|
|
5
5
|
const { parseType } = require('@strapi/utils');
|
|
6
|
+
const { ValidationError } = require('@strapi/utils').errors;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A GraphQL scalar used to store Time (HH:mm:ss.SSS) values
|
|
@@ -23,7 +24,7 @@ const TimeScalar = new GraphQLScalarType({
|
|
|
23
24
|
|
|
24
25
|
parseLiteral(ast) {
|
|
25
26
|
if (ast.kind !== Kind.STRING) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new ValidationError('Time cannot represent non string type');
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const value = ast.value;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { objectType } = require('nexus');
|
|
4
4
|
const { get } = require('lodash/fp');
|
|
5
|
+
const { ValidationError } = require('@strapi/utils').errors;
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Build an Error object type
|
|
@@ -20,7 +21,7 @@ module.exports = ({ strapi }) => {
|
|
|
20
21
|
|
|
21
22
|
const isValidPlaceholderCode = Object.values(ERROR_CODES).includes(code);
|
|
22
23
|
if (!isValidPlaceholderCode) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new ValidationError(`"${code}" is not a valid code value`);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
return code;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isFunction } = require('lodash/fp');
|
|
4
|
+
const { ApplicationError } = require('@strapi/utils').errors;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef RegisteredTypeDef
|
|
@@ -25,7 +26,7 @@ const createTypeRegistry = () => {
|
|
|
25
26
|
*/
|
|
26
27
|
register(name, definition, config = {}) {
|
|
27
28
|
if (registry.has(name)) {
|
|
28
|
-
throw new
|
|
29
|
+
throw new ApplicationError(`"${name}" has already been registered`);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
registry.set(name, { name, definition, config });
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { get, difference } = require('lodash/fp');
|
|
4
|
+
const { ApplicationError } = require('@strapi/utils').errors;
|
|
4
5
|
|
|
5
6
|
module.exports = ({ strapi }) => {
|
|
6
7
|
const { STRAPI_SCALARS, SCALARS_ASSOCIATIONS } = strapi.plugin('graphql').service('constants');
|
|
@@ -8,7 +9,7 @@ module.exports = ({ strapi }) => {
|
|
|
8
9
|
const missingStrapiScalars = difference(STRAPI_SCALARS, Object.keys(SCALARS_ASSOCIATIONS));
|
|
9
10
|
|
|
10
11
|
if (missingStrapiScalars.length > 0) {
|
|
11
|
-
throw new
|
|
12
|
+
throw new ApplicationError('Some Strapi scalars are not handled in the GraphQL scalars mapper');
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
return {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { camelCase, upperFirst, lowerFirst, pipe, get } = require('lodash/fp');
|
|
4
4
|
const { singular } = require('pluralize');
|
|
5
|
+
const { ApplicationError } = require('@strapi/utils').errors;
|
|
5
6
|
|
|
6
7
|
module.exports = ({ strapi }) => {
|
|
7
8
|
/**
|
|
@@ -222,7 +223,7 @@ module.exports = ({ strapi }) => {
|
|
|
222
223
|
const { prefix = '', suffix = '', plurality = 'singular', firstLetterCase = 'upper' } = options;
|
|
223
224
|
|
|
224
225
|
if (!['plural', 'singular'].includes(plurality)) {
|
|
225
|
-
throw new
|
|
226
|
+
throw new ApplicationError(
|
|
226
227
|
`"plurality" param must be either "plural" or "singular", but got: "${plurality}"`
|
|
227
228
|
);
|
|
228
229
|
}
|
package/strapi-server.js
CHANGED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg width="25px" height="28px" viewBox="0 0 25 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
3
|
-
<!-- Generator: Sketch 46.1 (44463) - http://www.bohemiancoding.com/sketch -->
|
|
4
|
-
<title>GraphQL_Logo</title>
|
|
5
|
-
<desc>Created with Sketch.</desc>
|
|
6
|
-
<defs></defs>
|
|
7
|
-
<g id="Pages" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
8
|
-
<g id="List-plugins" transform="translate(-302.000000, -668.000000)" fill-rule="nonzero" fill="#E535AB">
|
|
9
|
-
<g id="Container" transform="translate(263.000000, 83.000000)">
|
|
10
|
-
<g id="Content">
|
|
11
|
-
<g id="Forms" transform="translate(1.000000, 78.000000)">
|
|
12
|
-
<g id="List" transform="translate(15.000000, 19.000000)">
|
|
13
|
-
<g id="9" transform="translate(0.000000, 484.000000)">
|
|
14
|
-
<g id="GraphQL_Logo" transform="translate(23.000000, 4.000000)">
|
|
15
|
-
<rect id="Rectangle-path" transform="translate(7.525343, 11.130007) rotate(-149.999272) translate(-7.525343, -11.130007) " x="6.93249876" y="-0.309026904" width="1.1856882" height="22.8780681"></rect>
|
|
16
|
-
<rect id="Rectangle-path" x="1.05714286" y="19.1571429" width="22.8785714" height="1.18571429"></rect>
|
|
17
|
-
<rect id="Rectangle-path" transform="translate(7.528064, 22.613421) rotate(-149.999272) translate(-7.528064, -22.613421) " x="0.921066787" y="22.0205773" width="13.213995" height="1.1856882"></rect>
|
|
18
|
-
<rect id="Rectangle-path" transform="translate(17.469093, 5.393493) rotate(-149.999272) translate(-17.469093, -5.393493) " x="10.8620954" y="4.80064876" width="13.213995" height="1.1856882"></rect>
|
|
19
|
-
<rect id="Rectangle-path" transform="translate(7.531236, 5.388979) rotate(-120.000728) translate(-7.531236, -5.388979) " x="6.93839161" y="-1.21801893" width="1.1856882" height="13.213995"></rect>
|
|
20
|
-
<rect id="Rectangle-path" transform="translate(17.477229, 11.130221) rotate(-120.000728) translate(-17.477229, -11.130221) " x="6.03819452" y="10.5373773" width="22.8780681" height="1.1856882"></rect>
|
|
21
|
-
<rect id="Rectangle-path" x="1.96428571" y="7.39285714" width="1.18571429" height="13.2142857"></rect>
|
|
22
|
-
<rect id="Rectangle-path" x="21.85" y="7.39285714" width="1.18571429" height="13.2142857"></rect>
|
|
23
|
-
<rect id="Rectangle-path" transform="translate(17.469043, 22.606829) rotate(-120.000728) translate(-17.469043, -22.606829) " x="16.9511971" y="16.8605264" width="1.0356915" height="11.4926043"></rect>
|
|
24
|
-
<path d="M24.6071429,20.9928571 C23.9214286,22.1857143 22.3928571,22.5928571 21.2,21.9071429 C20.0071429,21.2214286 19.6,19.6928571 20.2857143,18.5 C20.9714286,17.3071429 22.5,16.9 23.6928571,17.5857143 C24.8928571,18.2785714 25.3,19.8 24.6071429,20.9928571" id="Shape"></path>
|
|
25
|
-
<path d="M4.70714286,9.5 C4.02142857,10.6928571 2.49285714,11.1 1.3,10.4142857 C0.107142857,9.72857143 -0.3,8.2 0.385714286,7.00714286 C1.07142857,5.81428571 2.6,5.40714286 3.79285714,6.09285714 C4.98571429,6.78571429 5.39285714,8.30714286 4.70714286,9.5" id="Shape"></path>
|
|
26
|
-
<path d="M0.392857143,20.9928571 C-0.292857143,19.8 0.114285714,18.2785714 1.30714286,17.5857143 C2.5,16.9 4.02142857,17.3071429 4.71428571,18.5 C5.4,19.6928571 4.99285714,21.2142857 3.8,21.9071429 C2.6,22.5928571 1.07857143,22.1857143 0.392857143,20.9928571" id="Shape"></path>
|
|
27
|
-
<path d="M20.2928571,9.5 C19.6071429,8.30714286 20.0142857,6.78571429 21.2071429,6.09285714 C22.4,5.40714286 23.9214286,5.81428571 24.6142857,7.00714286 C25.3,8.2 24.8928571,9.72142857 23.7,10.4142857 C22.5071429,11.1 20.9785714,10.6928571 20.2928571,9.5" id="Shape"></path>
|
|
28
|
-
<path d="M12.5,27.9857143 C11.1214286,27.9857143 10.0071429,26.8714286 10.0071429,25.4928571 C10.0071429,24.1142857 11.1214286,23 12.5,23 C13.8785714,23 14.9928571,24.1142857 14.9928571,25.4928571 C14.9928571,26.8642857 13.8785714,27.9857143 12.5,27.9857143" id="Shape"></path>
|
|
29
|
-
<path d="M12.5,5 C11.1214286,5 10.0071429,3.88571429 10.0071429,2.50714286 C10.0071429,1.12857143 11.1214286,0.0142857143 12.5,0.0142857143 C13.8785714,0.0142857143 14.9928571,1.12857143 14.9928571,2.50714286 C14.9928571,3.88571429 13.8785714,5 12.5,5" id="Shape"></path>
|
|
30
|
-
</g>
|
|
31
|
-
</g>
|
|
32
|
-
</g>
|
|
33
|
-
</g>
|
|
34
|
-
</g>
|
|
35
|
-
</g>
|
|
36
|
-
</g>
|
|
37
|
-
</g>
|
|
38
|
-
</svg>
|