strapi-plugin-comments 2.0.0-beta.5 → 2.0.0-rc.2
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 +472 -118
- package/admin/src/components/DiscussionThread/index.js +9 -3
- package/admin/src/components/DiscussionThreadItem/index.js +24 -11
- package/admin/src/components/DiscussionThreadItemActions/index.js +45 -18
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +20 -11
- package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +15 -9
- package/admin/src/components/ReportsReviewModal/index.js +5 -0
- package/admin/src/components/ReportsReviewTable/index.js +60 -52
- package/admin/src/components/icons/review.js +1 -1
- package/admin/src/index.js +1 -1
- package/admin/src/pages/Details/components/DetailsEntity/index.js +2 -2
- package/admin/src/pages/Details/components/DetailsFilters/index.js +1 -0
- package/admin/src/pages/Details/index.js +43 -37
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +38 -11
- package/admin/src/pages/Discover/index.js +72 -59
- package/admin/src/permissions.js +10 -8
- package/admin/src/translations/en.json +2 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/renderInitials.js +5 -0
- package/content-types/comment.js +0 -4
- package/package.json +4 -2
- package/permissions.js +19 -0
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/bootstrap.js +38 -2
- package/server/config/index.js +7 -2
- package/server/controllers/client.js +11 -21
- package/server/controllers/utils/parsers.js +19 -0
- package/server/graphql/index.js +26 -0
- package/server/graphql/mutations/createAbuseReport.js +25 -0
- package/server/graphql/mutations/createComment.js +25 -0
- package/server/graphql/mutations/index.js +21 -0
- package/server/graphql/mutations/removeComment.js +25 -0
- package/server/graphql/mutations/updateComment.js +25 -0
- package/server/graphql/queries/findAllFlat.js +29 -0
- package/server/graphql/queries/findAllInHierarchy.js +31 -0
- package/server/graphql/queries/index.js +19 -0
- package/server/graphql/resolvers-config.js +15 -0
- package/server/graphql/types/comment-author.js +9 -0
- package/server/graphql/types/comment-nested.js +15 -0
- package/server/graphql/types/comment.js +15 -0
- package/server/graphql/types/create-comment-author.js +9 -0
- package/server/graphql/types/create-comment.js +9 -0
- package/server/graphql/types/create-report.js +9 -0
- package/server/graphql/types/id.js +24 -0
- package/server/graphql/types/identify-comment-author.js +6 -0
- package/server/graphql/types/index.js +16 -0
- package/server/graphql/types/remove-comment.js +8 -0
- package/server/graphql/types/report-reason.js +10 -0
- package/server/graphql/types/report.js +11 -0
- package/server/graphql/types/update-comment.js +9 -0
- package/server/register.js +3 -1
- package/server/services/admin.js +20 -16
- package/server/services/client.js +72 -49
- package/server/services/common.js +23 -12
- package/server/services/graphql.js +190 -0
- package/server/services/index.js +2 -0
- package/server/services/utils/functions.js +30 -11
- package/strapi-server.js +33 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const config = require('
|
|
3
|
+
const config = require('../config');
|
|
4
4
|
const { getPluginService } = require('./../utils/functions');
|
|
5
|
-
const { isNil, isEmpty } = require('lodash');
|
|
5
|
+
const { isNil, isEmpty, isNumber, parseInt } = require('lodash');
|
|
6
6
|
const PluginError = require('./../utils/error');
|
|
7
7
|
const {
|
|
8
8
|
isEqualEntity,
|
|
9
9
|
getModelUid,
|
|
10
10
|
getRelatedGroups,
|
|
11
11
|
checkBadWords,
|
|
12
|
-
isValidUserContext,
|
|
13
12
|
resolveUserContextError,
|
|
14
13
|
} = require('./utils/functions');
|
|
15
14
|
const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
@@ -34,7 +33,8 @@ module.exports = ({ strapi }) => ({
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
const [ uid, relatedStringId ] = getRelatedGroups(relation);
|
|
37
|
-
const
|
|
36
|
+
const parsedRelatedId = parseInt(relatedStringId);
|
|
37
|
+
const relatedId = isNumber(parsedRelatedId) ? parsedRelatedId : relatedStringId;
|
|
38
38
|
|
|
39
39
|
const relatedEntity = await strapi.db.query(uid).findOne(relatedId);
|
|
40
40
|
if (!relatedEntity) {
|
|
@@ -44,11 +44,11 @@ module.exports = ({ strapi }) => ({
|
|
|
44
44
|
const isApprovalFlowEnabled = this.getCommonService().getConfig('approvalFlow', []).includes(uid) ||
|
|
45
45
|
relatedEntity.requireCommentsApproval;
|
|
46
46
|
|
|
47
|
-
const linkToThread = !isNil(threadOf) ?
|
|
47
|
+
const linkToThread = !isNil(threadOf) ? await this.getCommonService().findOne({
|
|
48
48
|
id: threadOf,
|
|
49
49
|
related: relation,
|
|
50
50
|
}) : true;
|
|
51
|
-
const validContext = isValidUserContext(user);
|
|
51
|
+
const validContext = this.getCommonService().isValidUserContext(user);
|
|
52
52
|
|
|
53
53
|
if (!linkToThread) {
|
|
54
54
|
throw new PluginError(400, 'Thread does not exist');
|
|
@@ -59,15 +59,11 @@ module.exports = ({ strapi }) => ({
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (checkBadWords(content) && singleRelationFulfilled) {
|
|
62
|
-
const { author
|
|
62
|
+
const { author = {}, ...rest } = data;
|
|
63
63
|
let authorData = {};
|
|
64
64
|
if (validContext && user) {
|
|
65
65
|
authorData = {
|
|
66
|
-
authorUser: user?.id
|
|
67
|
-
};
|
|
68
|
-
} else if (authorUser) {
|
|
69
|
-
authorData = {
|
|
70
|
-
authorUser,
|
|
66
|
+
authorUser: user?.id,
|
|
71
67
|
};
|
|
72
68
|
} else {
|
|
73
69
|
if (author.email && !REGEX.email.test(author.email)) {
|
|
@@ -82,8 +78,8 @@ module.exports = ({ strapi }) => ({
|
|
|
82
78
|
};
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
if (!isEmpty(authorData) && !authorData.authorId) {
|
|
86
|
-
throw new PluginError(400, '
|
|
81
|
+
if (!isEmpty(authorData) && !(authorData.authorId || authorData.authorUser)) {
|
|
82
|
+
throw new PluginError(400, `Not able to recognise author of a comment. Make sure you've provided "author" property in a payload or authenticated your request properly.`);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
if (isApprovalFlowEnabled &&
|
|
@@ -97,32 +93,35 @@ module.exports = ({ strapi }) => ({
|
|
|
97
93
|
...authorData,
|
|
98
94
|
related: relation,
|
|
99
95
|
approvalStatus: isApprovalFlowEnabled ? APPROVAL_STATUS.PENDING : null,
|
|
100
|
-
}
|
|
96
|
+
},
|
|
97
|
+
populate: {
|
|
98
|
+
authorUser: true,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return this.getCommonService().sanitizeCommentEntity({
|
|
102
|
+
...entity,
|
|
103
|
+
threadOf: !isNil(threadOf) ? linkToThread : null,
|
|
101
104
|
});
|
|
102
|
-
return this.getCommonService().sanitizeCommentEntity(entity);
|
|
103
105
|
}
|
|
104
106
|
throw new PluginError(400, 'No content received');
|
|
105
107
|
},
|
|
106
108
|
|
|
107
109
|
// Update a comment
|
|
108
|
-
async update(id, relation, data, user) {
|
|
109
|
-
const { content
|
|
110
|
+
async update(id, relation, data, user = undefined) {
|
|
111
|
+
const { content } = data;
|
|
110
112
|
|
|
111
|
-
const singleRelationFulfilled =
|
|
112
|
-
|
|
113
|
-
if (relation !== related) {
|
|
114
|
-
throw new PluginError(400, `Insonsistent relation uid`);
|
|
115
|
-
}
|
|
113
|
+
const singleRelationFulfilled = relation && REGEX.relatedUid.test(relation)
|
|
116
114
|
|
|
117
115
|
if(!singleRelationFulfilled) {
|
|
118
|
-
throw new PluginError(400, `
|
|
116
|
+
throw new PluginError(400, `Request property "relation" got incorrect format, use format like "api::<collection name>.<content type name>:<entity id>"`);
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
const existingEntity = await this.getCommonService().findOne({
|
|
122
120
|
id,
|
|
123
|
-
related,
|
|
121
|
+
related: relation,
|
|
124
122
|
});
|
|
125
|
-
|
|
123
|
+
|
|
124
|
+
const validContext = this.getCommonService().isValidUserContext(user);
|
|
126
125
|
|
|
127
126
|
if (!validContext) {
|
|
128
127
|
throw resolveUserContextError(user);
|
|
@@ -133,23 +132,24 @@ module.exports = ({ strapi }) => ({
|
|
|
133
132
|
const entity = await strapi.db.query(getModelUid('comment')).update({
|
|
134
133
|
where: { id },
|
|
135
134
|
data: { content },
|
|
135
|
+
populate: { threadOf: true, authorUser: true },
|
|
136
136
|
});
|
|
137
137
|
return this.getCommonService().sanitizeCommentEntity(entity);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
throw new PluginError(
|
|
140
|
+
throw new PluginError(403, `You're not allowed to take an action on that entity. Make sure you've provided "author" property in a payload or authenticated your request properly.`);
|
|
141
141
|
},
|
|
142
142
|
|
|
143
143
|
// Report abuse in comment
|
|
144
|
-
async reportAbuse(id, relation, payload, user) {
|
|
145
|
-
if (!isValidUserContext(user)) {
|
|
144
|
+
async reportAbuse(id, relation, payload, user = undefined) {
|
|
145
|
+
if (!this.getCommonService().isValidUserContext(user)) {
|
|
146
146
|
throw resolveUserContextError(user);
|
|
147
147
|
}
|
|
148
|
-
const
|
|
148
|
+
const reportAgainstEntity = await this.getCommonService().findOne({
|
|
149
149
|
id,
|
|
150
150
|
related: relation,
|
|
151
151
|
});
|
|
152
|
-
if (
|
|
152
|
+
if (reportAgainstEntity) {
|
|
153
153
|
const entity = await strapi.db.query(getModelUid('comment-report')).create({
|
|
154
154
|
data: {
|
|
155
155
|
...payload,
|
|
@@ -158,39 +158,62 @@ module.exports = ({ strapi }) => ({
|
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
if (entity) {
|
|
161
|
+
const response = {
|
|
162
|
+
...entity,
|
|
163
|
+
related: reportAgainstEntity,
|
|
164
|
+
};
|
|
161
165
|
try {
|
|
162
166
|
await this.sendAbuseReportEmail(entity.reason, entity.content); // Could also add some info about relation
|
|
167
|
+
return response
|
|
163
168
|
} catch (err) {
|
|
164
|
-
return
|
|
169
|
+
return response;
|
|
165
170
|
}
|
|
166
171
|
} else {
|
|
167
172
|
throw new PluginError(500, 'Report cannot be created');
|
|
168
173
|
}
|
|
169
|
-
return entity;
|
|
170
174
|
}
|
|
171
|
-
throw new PluginError(
|
|
175
|
+
throw new PluginError(403, `You're not allowed to take an action on that entity. Make sure that comment exist or you've authenticated your request properly.`);
|
|
172
176
|
},
|
|
173
177
|
|
|
174
|
-
async markAsRemoved(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
async markAsRemoved(id, relation, authorId, user = undefined){
|
|
179
|
+
if (!this.getCommonService().isValidUserContext(user)) {
|
|
180
|
+
throw resolveUserContextError(user);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const author = user?.id || authorId;
|
|
184
|
+
|
|
185
|
+
if (!author) {
|
|
186
|
+
throw new PluginError(403, `You're not allowed to take an action on that entity. Make sure that you've provided proper "authorId" or authenticated your request properly.`);
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
|
|
189
|
+
let entity;
|
|
190
|
+
try {
|
|
191
|
+
const byAuthor = user?.id ? {
|
|
192
|
+
authorUser: author
|
|
193
|
+
} : {
|
|
194
|
+
authorId: author
|
|
195
|
+
}
|
|
196
|
+
entity = await this.getCommonService().findOne({
|
|
197
|
+
id,
|
|
198
|
+
related: relation,
|
|
199
|
+
...byAuthor,
|
|
200
|
+
});
|
|
201
|
+
} catch(e) {
|
|
202
|
+
throw new PluginError(404, `Entity does not exist or you're not allowed to take an action on it`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const removedEntity = await strapi.db.query(getModelUid('comment'))
|
|
185
206
|
.update({
|
|
186
207
|
where: {
|
|
187
|
-
id
|
|
188
|
-
related:
|
|
208
|
+
id,
|
|
209
|
+
related: relation
|
|
189
210
|
},
|
|
190
|
-
data: { removed: true }
|
|
211
|
+
data: { removed: true },
|
|
212
|
+
populate: { threadOf: true, authorUser: true },
|
|
191
213
|
})
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
await this.markAsRemovedNested(id, true);
|
|
215
|
+
|
|
216
|
+
return this.getCommonService().sanitizeCommentEntity(removedEntity);
|
|
194
217
|
},
|
|
195
218
|
|
|
196
219
|
async sendAbuseReportEmail(reason, content) {
|
|
@@ -229,7 +252,7 @@ module.exports = ({ strapi }) => ({
|
|
|
229
252
|
}
|
|
230
253
|
},
|
|
231
254
|
|
|
232
|
-
markAsRemovedNested(id, status){
|
|
255
|
+
async markAsRemovedNested(id, status){
|
|
233
256
|
return this.getCommonService().modifiedNestedNestedComments(id, 'removed', status);
|
|
234
257
|
},
|
|
235
258
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isArray,
|
|
3
|
+
const { isArray, isNumber, isObject, isNil, first, parseInt } = require('lodash');
|
|
4
4
|
// const { sanitizeEntity } = require('strapi-utils');
|
|
5
5
|
const PluginError = require('./../utils/error');
|
|
6
6
|
const {
|
|
7
7
|
getModelUid,
|
|
8
8
|
getRelatedGroups,
|
|
9
9
|
buildNestedStructure,
|
|
10
|
-
filterOurResolvedReports
|
|
10
|
+
filterOurResolvedReports,
|
|
11
|
+
buildAuthorModel,
|
|
11
12
|
} = require('./utils/functions');
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -85,8 +86,12 @@ module.exports = ({ strapi }) => ({
|
|
|
85
86
|
async findOne(criteria) {
|
|
86
87
|
const entity = await strapi.db.query(getModelUid('comment'))
|
|
87
88
|
.findOne({
|
|
88
|
-
where: criteria
|
|
89
|
-
|
|
89
|
+
where: criteria,
|
|
90
|
+
populate: {
|
|
91
|
+
reports: true,
|
|
92
|
+
authorUser: true,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
90
95
|
if (!entity){
|
|
91
96
|
throw new PluginError(404, 'Not found');
|
|
92
97
|
}
|
|
@@ -97,9 +102,11 @@ module.exports = ({ strapi }) => ({
|
|
|
97
102
|
async findRelatedEntitiesFor(entities = []) {
|
|
98
103
|
const data = entities.reduce((acc, cur) => {
|
|
99
104
|
const [relatedUid, relatedStringId] = getRelatedGroups(cur.related);
|
|
105
|
+
const parsedRelatedId = parseInt(relatedStringId);
|
|
106
|
+
const relatedId = isNumber(parsedRelatedId) ? parsedRelatedId : relatedStringId;
|
|
100
107
|
return {
|
|
101
108
|
...acc,
|
|
102
|
-
[relatedUid]: [...(acc[relatedUid] || []),
|
|
109
|
+
[relatedUid]: [...(acc[relatedUid] || []), relatedId]
|
|
103
110
|
};
|
|
104
111
|
},
|
|
105
112
|
{}
|
|
@@ -151,12 +158,16 @@ module.exports = ({ strapi }) => ({
|
|
|
151
158
|
}
|
|
152
159
|
},
|
|
153
160
|
|
|
154
|
-
sanitizeCommentEntity
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
sanitizeCommentEntity(entity) {
|
|
162
|
+
return {
|
|
163
|
+
...buildAuthorModel({
|
|
164
|
+
...entity,
|
|
165
|
+
threadOf: isObject(entity.threadOf) ? buildAuthorModel(entity.threadOf) : entity.threadOf,
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
},
|
|
157
169
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// }),
|
|
170
|
+
isValidUserContext(user) {
|
|
171
|
+
return user ? !isNil(user?.id) : true;
|
|
172
|
+
},
|
|
162
173
|
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { has, propEq, isNil, isDate, isObject } = require('lodash/fp');
|
|
4
|
+
const { inputObjectType } = require('nexus');
|
|
5
|
+
|
|
6
|
+
const virtualScalarAttributes = ['id'];
|
|
7
|
+
|
|
8
|
+
module.exports = ({ strapi }) => {
|
|
9
|
+
const { service: getService } = strapi.plugin('graphql');
|
|
10
|
+
|
|
11
|
+
const rootLevelOperators = () => {
|
|
12
|
+
const { operators } = strapi.plugin('graphql').service('builders').filters;
|
|
13
|
+
|
|
14
|
+
return [operators.and, operators.or, operators.not];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const buildContentTypeFilters = contentType => {
|
|
18
|
+
const utils = strapi.plugin('graphql').service('utils');
|
|
19
|
+
const extension = strapi.plugin('graphql').service('extension');
|
|
20
|
+
|
|
21
|
+
const { getFiltersInputTypeName, getScalarFilterInputTypeName } = utils.naming;
|
|
22
|
+
const { isStrapiScalar, isRelation } = utils.attributes;
|
|
23
|
+
|
|
24
|
+
const { attributes } = contentType;
|
|
25
|
+
|
|
26
|
+
const filtersTypeName = getFiltersInputTypeName(contentType);
|
|
27
|
+
|
|
28
|
+
return inputObjectType({
|
|
29
|
+
name: filtersTypeName,
|
|
30
|
+
|
|
31
|
+
definition(t) {
|
|
32
|
+
const validAttributes = Object.entries(attributes).filter(([attributeName]) =>
|
|
33
|
+
extension
|
|
34
|
+
.shadowCRUD(contentType.uid)
|
|
35
|
+
.field(attributeName)
|
|
36
|
+
.hasFiltersEnabeld()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const isIDFilterEnabled = extension
|
|
40
|
+
.shadowCRUD(contentType.uid)
|
|
41
|
+
.field('id')
|
|
42
|
+
.hasFiltersEnabeld();
|
|
43
|
+
// Add an ID filter to the collection types
|
|
44
|
+
if (contentType.kind === 'collectionType' && isIDFilterEnabled) {
|
|
45
|
+
t.field('id', { type: getScalarFilterInputTypeName('ID') });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add every defined attribute
|
|
49
|
+
for (const [attributeName, attribute] of validAttributes) {
|
|
50
|
+
// Handle scalars
|
|
51
|
+
if (isStrapiScalar(attribute)) {
|
|
52
|
+
addScalarAttribute(t, attributeName, attribute);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle relations
|
|
56
|
+
else if (isRelation(attribute)) {
|
|
57
|
+
addRelationalAttribute(t, attributeName, attribute);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Conditional clauses
|
|
62
|
+
for (const operator of rootLevelOperators()) {
|
|
63
|
+
operator.add(t, filtersTypeName);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const addScalarAttribute = (builder, attributeName, attribute) => {
|
|
70
|
+
const { naming, mappers } = strapi.plugin('graphql').service('utils');
|
|
71
|
+
|
|
72
|
+
const gqlType = mappers.strapiScalarToGraphQLScalar(attribute.type);
|
|
73
|
+
|
|
74
|
+
builder.field(attributeName, { type: naming.getScalarFilterInputTypeName(gqlType) });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const addRelationalAttribute = (builder, attributeName, attribute) => {
|
|
78
|
+
const utils = strapi.plugin('graphql').service('utils');
|
|
79
|
+
const extension = strapi.plugin('graphql').service('extension');
|
|
80
|
+
const { getFiltersInputTypeName } = utils.naming;
|
|
81
|
+
const { isMorphRelation } = utils.attributes;
|
|
82
|
+
|
|
83
|
+
const model = strapi.getModel(attribute.target);
|
|
84
|
+
|
|
85
|
+
// If there is no model corresponding to the attribute configuration
|
|
86
|
+
// or if the attribute is a polymorphic relation, then ignore it
|
|
87
|
+
if (!model || isMorphRelation(attribute)) return;
|
|
88
|
+
|
|
89
|
+
// If the target model is disabled, then ignore it too
|
|
90
|
+
if (extension.shadowCRUD(model.uid).isDisabled()) return;
|
|
91
|
+
|
|
92
|
+
builder.field(attributeName, { type: getFiltersInputTypeName(model) });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const recursivelyReplaceScalarOperators = data => {
|
|
96
|
+
const { operators } = getService('builders').filters;
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(data)) {
|
|
99
|
+
return data.map(recursivelyReplaceScalarOperators);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Note: We need to make an exception for date since GraphQL
|
|
103
|
+
// automatically cast date strings to date instances in args
|
|
104
|
+
if (isDate(data) || !isObject(data)) {
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = {};
|
|
109
|
+
|
|
110
|
+
for (const [key, value] of Object.entries(data)) {
|
|
111
|
+
const isOperator = !!operators[key];
|
|
112
|
+
|
|
113
|
+
const newKey = isOperator ? operators[key].strapiOperator : key;
|
|
114
|
+
|
|
115
|
+
result[newKey] = recursivelyReplaceScalarOperators(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const graphQLFiltersToStrapiQuery = (filters, contentType = {}) => {
|
|
122
|
+
const { isStrapiScalar, isMedia, isRelation } = getService('utils').attributes;
|
|
123
|
+
const { operators } = getService('builders').filters;
|
|
124
|
+
|
|
125
|
+
const ROOT_LEVEL_OPERATORS = [operators.and, operators.or, operators.not];
|
|
126
|
+
|
|
127
|
+
// Handle unwanted scenario where there is no filters defined
|
|
128
|
+
if (isNil(filters)) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If filters is a collection, then apply the transformation to every item of the list
|
|
133
|
+
if (Array.isArray(filters)) {
|
|
134
|
+
return filters.map(filtersItem =>
|
|
135
|
+
graphQLFiltersToStrapiQuery(filtersItem, contentType)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resultMap = {};
|
|
140
|
+
const { attributes } = contentType;
|
|
141
|
+
|
|
142
|
+
const isAttribute = attributeName => {
|
|
143
|
+
return virtualScalarAttributes.includes(attributeName) || has(attributeName, attributes);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
147
|
+
// If the key is an attribute, update the value
|
|
148
|
+
if (isAttribute(key)) {
|
|
149
|
+
const attribute = attributes[key];
|
|
150
|
+
|
|
151
|
+
// If it's a scalar attribute
|
|
152
|
+
if (virtualScalarAttributes.includes(key) || isStrapiScalar(attribute)) {
|
|
153
|
+
// Replace (recursively) every GraphQL scalar operator with the associated Strapi operator
|
|
154
|
+
resultMap[key] = recursivelyReplaceScalarOperators(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If it's a deep filter on a relation
|
|
158
|
+
else if (isRelation(attribute) || isMedia(attribute)) {
|
|
159
|
+
// Fetch the model from the relation
|
|
160
|
+
const relModel = strapi.getModel(attribute.target);
|
|
161
|
+
|
|
162
|
+
// Recursively apply the mapping to the value using the fetched model,
|
|
163
|
+
// and update the value within `resultMap`
|
|
164
|
+
resultMap[key] = graphQLFiltersToStrapiQuery(value, relModel);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle the case where the key is not an attribute (operator, ...)
|
|
169
|
+
else {
|
|
170
|
+
const rootLevelOperator = ROOT_LEVEL_OPERATORS.find(propEq('fieldName', key));
|
|
171
|
+
|
|
172
|
+
// If it's a root level operator (AND, NOT, OR, ...)
|
|
173
|
+
if (rootLevelOperator) {
|
|
174
|
+
const { strapiOperator } = rootLevelOperator;
|
|
175
|
+
|
|
176
|
+
// Transform the current value recursively and add it to the resultMap
|
|
177
|
+
// object using the strapiOperator equivalent of the GraphQL key
|
|
178
|
+
resultMap[strapiOperator] = graphQLFiltersToStrapiQuery(value, contentType);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return resultMap;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
buildContentTypeFilters,
|
|
188
|
+
graphQLFiltersToStrapiQuery,
|
|
189
|
+
};
|
|
190
|
+
};
|
package/server/services/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
const adminServices = require('./admin');
|
|
4
4
|
const clientServices = require('./client');
|
|
5
5
|
const commonServices = require('./common');
|
|
6
|
+
const graphQLService = require('./graphql');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
admin: adminServices,
|
|
9
10
|
client: clientServices,
|
|
10
11
|
common: commonServices,
|
|
12
|
+
gql: graphQLService,
|
|
11
13
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const BadWordsFilter = require('bad-words');
|
|
2
2
|
const PluginError = require('./../../utils/error');
|
|
3
3
|
const { REGEX } = require('./../../utils/constants')
|
|
4
|
-
const { first, get, isObject,
|
|
4
|
+
const { first, get, isObject, isEmpty } = require('lodash');
|
|
5
5
|
|
|
6
6
|
const buildNestedStructure = (
|
|
7
7
|
entities,
|
|
8
8
|
id = null,
|
|
9
|
-
field = '
|
|
9
|
+
field = 'threadOf',
|
|
10
10
|
dropBlockedThreads = false,
|
|
11
11
|
blockNestedThreads = false,
|
|
12
12
|
) =>
|
|
@@ -33,7 +33,7 @@ const buildNestedStructure = (
|
|
|
33
33
|
|
|
34
34
|
module.exports = {
|
|
35
35
|
isEqualEntity: (existing, data, user) => {
|
|
36
|
-
const {
|
|
36
|
+
const { author: existingAuthor } = existing;
|
|
37
37
|
const { author } = data;
|
|
38
38
|
|
|
39
39
|
// Disallow approval status change by Client
|
|
@@ -42,12 +42,12 @@ module.exports = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Make sure that author is exact the same
|
|
45
|
-
if (
|
|
46
|
-
const existingUserId =
|
|
47
|
-
const receivedUserId = user?.id || author;
|
|
48
|
-
return existingUserId === receivedUserId;
|
|
45
|
+
if (user) {
|
|
46
|
+
const existingUserId = existingAuthor?.id || existingAuthor;
|
|
47
|
+
const receivedUserId = user?.id || author?.id;
|
|
48
|
+
return receivedUserId && (existingUserId === receivedUserId);
|
|
49
49
|
}
|
|
50
|
-
return
|
|
50
|
+
return existingAuthor.id === author?.id;
|
|
51
51
|
},
|
|
52
52
|
|
|
53
53
|
getRelatedGroups: related => related.split(REGEX.relatedUid).filter(s => s && s.length > 0),
|
|
@@ -85,9 +85,28 @@ module.exports = {
|
|
|
85
85
|
|
|
86
86
|
buildNestedStructure,
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
buildAuthorModel: (item) => {
|
|
89
|
+
const { authorUser, authorId, authorName, authorEmail, authorAvatar, ...rest } = item;
|
|
90
|
+
let author = {};
|
|
91
|
+
if (authorUser) {
|
|
92
|
+
author = {
|
|
93
|
+
id: authorUser.id,
|
|
94
|
+
name: authorUser.username,
|
|
95
|
+
email: authorUser.email,
|
|
96
|
+
avatar: authorUser.avatar,
|
|
97
|
+
};
|
|
98
|
+
} else if(authorId) {
|
|
99
|
+
author = {
|
|
100
|
+
id: authorId,
|
|
101
|
+
name: authorName,
|
|
102
|
+
email: authorEmail,
|
|
103
|
+
avatar: authorAvatar,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...rest,
|
|
108
|
+
author: isEmpty(author) ? undefined : author,
|
|
109
|
+
};
|
|
91
110
|
},
|
|
92
111
|
|
|
93
112
|
resolveUserContextError: user => {
|
package/strapi-server.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { isArray, isEmpty, isString } = require('lodash');
|
|
4
|
+
|
|
3
5
|
const server = require('./server');
|
|
4
6
|
const contentTypes = require('./content-types');
|
|
7
|
+
const { REGEX } = require('./server/utils/constants');
|
|
5
8
|
|
|
6
9
|
module.exports = () => ({
|
|
7
10
|
...server,
|
|
8
11
|
contentTypes,
|
|
12
|
+
config: {
|
|
13
|
+
...server.config,
|
|
14
|
+
default: ({ env }) => ({
|
|
15
|
+
...server.config.default,
|
|
16
|
+
}),
|
|
17
|
+
validator: (config) => {
|
|
18
|
+
// Check moderatorRoles values
|
|
19
|
+
const moderatorRolesValid = config.moderatorRoles
|
|
20
|
+
.filter(_ => !isString(_)).length === 0;
|
|
21
|
+
if (!moderatorRolesValid) {
|
|
22
|
+
throw new Error(`'moderatorRoles' must roles keys`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check approvalFlow values
|
|
26
|
+
const approvalFlowValid = config.approvalFlow
|
|
27
|
+
.filter(_ => !REGEX.uid.test(_)).length === 0;
|
|
28
|
+
if (!approvalFlowValid) {
|
|
29
|
+
throw new Error(`'approvalFlow' must contain Content Types identifiers in the format like 'api::<collection name>.<content type name>'`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check entryLabel keys and values
|
|
33
|
+
const entryLabelKeysValid = Object.keys(config.entryLabel)
|
|
34
|
+
.filter(_ => _ !== '*')
|
|
35
|
+
.filter(_ => !(REGEX.uid.test(_) && isArray(config.entryLabel[_]) && !isEmpty(config.entryLabel[_]))).length === 0;
|
|
36
|
+
|
|
37
|
+
if (!entryLabelKeysValid) {
|
|
38
|
+
throw new Error(`'entryLabel' must contain records in the format like 'api::<collection name>.<content type name>': ['Field1', 'field_2']`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
9
42
|
});
|