@strapi/utils 4.11.0-beta.1 → 4.11.0-exp.9xg4-3qfm-9w8f.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/lib/content-types.js +11 -4
- package/lib/convert-query-params.js +35 -6
- package/lib/index.js +3 -2
- package/lib/operators.js +74 -0
- package/lib/sanitize/sanitizers.js +12 -0
- package/lib/sanitize/visitors/remove-private.js +1 -1
- package/package.json +2 -2
- package/lib/webhook.js +0 -16
package/lib/content-types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
|
-
const { has } = require('lodash/fp');
|
|
4
|
+
const { getOr, has, union } = require('lodash/fp');
|
|
5
5
|
|
|
6
6
|
const SINGLE_TYPE = 'singleType';
|
|
7
7
|
const COLLECTION_TYPE = 'collectionType';
|
|
@@ -92,16 +92,23 @@ const isSingleType = ({ kind = COLLECTION_TYPE }) => kind === SINGLE_TYPE;
|
|
|
92
92
|
const isCollectionType = ({ kind = COLLECTION_TYPE }) => kind === COLLECTION_TYPE;
|
|
93
93
|
const isKind = (kind) => (model) => model.kind === kind;
|
|
94
94
|
|
|
95
|
+
const getStoredPrivateAttributes = (model) => union(
|
|
96
|
+
strapi?.config?.get('api.responses.privateAttributes', []) ?? [],
|
|
97
|
+
getOr([], 'options.privateAttributes', model)
|
|
98
|
+
);
|
|
99
|
+
|
|
95
100
|
const getPrivateAttributes = (model = {}) => {
|
|
96
101
|
return _.union(
|
|
97
|
-
|
|
98
|
-
_.get(model, 'options.privateAttributes', []),
|
|
102
|
+
getStoredPrivateAttributes(model),
|
|
99
103
|
_.keys(_.pickBy(model.attributes, (attr) => !!attr.private))
|
|
100
104
|
);
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
const isPrivateAttribute = (model, attributeName) => {
|
|
104
|
-
|
|
108
|
+
if (model?.attributes?.[attributeName]?.private === true) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return getStoredPrivateAttributes(model).includes(attributeName);
|
|
105
112
|
};
|
|
106
113
|
|
|
107
114
|
const isScalarAttribute = (attribute) => {
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
cloneDeep,
|
|
19
19
|
get,
|
|
20
20
|
mergeAll,
|
|
21
|
+
isString,
|
|
21
22
|
} = require('lodash/fp');
|
|
22
23
|
const _ = require('lodash');
|
|
23
24
|
const parseType = require('./parse-type');
|
|
@@ -28,6 +29,7 @@ const {
|
|
|
28
29
|
isDynamicZoneAttribute,
|
|
29
30
|
isMorphToRelationalAttribute,
|
|
30
31
|
} = require('./content-types');
|
|
32
|
+
const { isOperator } = require('./operators');
|
|
31
33
|
|
|
32
34
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
|
33
35
|
|
|
@@ -46,7 +48,7 @@ class InvalidSortError extends Error {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const validateOrder = (order) => {
|
|
49
|
-
if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) {
|
|
51
|
+
if (!isString(order) || !['asc', 'desc'].includes(order.toLocaleLowerCase())) {
|
|
50
52
|
throw new InvalidOrderError();
|
|
51
53
|
}
|
|
52
54
|
};
|
|
@@ -80,6 +82,13 @@ const convertSortQueryParams = (sortQuery) => {
|
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
const convertSingleSortQueryParam = (sortQuery) => {
|
|
85
|
+
if (!sortQuery) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
if (!isString(sortQuery)) {
|
|
89
|
+
throw new Error('Invalid sort query');
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
// split field and order param with default order to ascending
|
|
84
93
|
const [field, order = 'asc'] = sortQuery.split(':');
|
|
85
94
|
|
|
@@ -89,6 +98,8 @@ const convertSingleSortQueryParam = (sortQuery) => {
|
|
|
89
98
|
|
|
90
99
|
validateOrder(order);
|
|
91
100
|
|
|
101
|
+
// TODO: field should be a valid path on an object model
|
|
102
|
+
|
|
92
103
|
return _.set({}, field, order);
|
|
93
104
|
};
|
|
94
105
|
|
|
@@ -368,6 +379,7 @@ const convertNestedPopulate = (subPopulate, schema) => {
|
|
|
368
379
|
return query;
|
|
369
380
|
};
|
|
370
381
|
|
|
382
|
+
// TODO: ensure field is valid in content types (will probably have to check strapi.contentTypes since it can be a string.path)
|
|
371
383
|
const convertFieldsQueryParams = (fields, depth = 0) => {
|
|
372
384
|
if (depth === 0 && fields === '*') {
|
|
373
385
|
return undefined;
|
|
@@ -387,6 +399,18 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
|
|
|
387
399
|
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
|
|
388
400
|
};
|
|
389
401
|
|
|
402
|
+
const isValidSchemaAttribute = (key, schema) => {
|
|
403
|
+
if (key === 'id') {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!schema) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return Object.keys(schema.attributes).includes(key);
|
|
412
|
+
};
|
|
413
|
+
|
|
390
414
|
const convertFiltersQueryParams = (filters, schema) => {
|
|
391
415
|
// Filters need to be either an array or an object
|
|
392
416
|
// Here we're only checking for 'object' type since typeof [] => object and typeof {} => object
|
|
@@ -401,10 +425,6 @@ const convertFiltersQueryParams = (filters, schema) => {
|
|
|
401
425
|
};
|
|
402
426
|
|
|
403
427
|
const convertAndSanitizeFilters = (filters, schema) => {
|
|
404
|
-
if (!isPlainObject(filters)) {
|
|
405
|
-
return filters;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
428
|
if (Array.isArray(filters)) {
|
|
409
429
|
return (
|
|
410
430
|
filters
|
|
@@ -415,14 +435,23 @@ const convertAndSanitizeFilters = (filters, schema) => {
|
|
|
415
435
|
);
|
|
416
436
|
}
|
|
417
437
|
|
|
438
|
+
// This must come after check for Array or else arrays are not filtered
|
|
439
|
+
if (!isPlainObject(filters)) {
|
|
440
|
+
return filters;
|
|
441
|
+
}
|
|
442
|
+
|
|
418
443
|
const removeOperator = (operator) => delete filters[operator];
|
|
419
444
|
|
|
420
445
|
// Here, `key` can either be an operator or an attribute name
|
|
421
446
|
for (const [key, value] of Object.entries(filters)) {
|
|
422
447
|
const attribute = get(key, schema?.attributes);
|
|
448
|
+
const validKey = isOperator(key) || isValidSchemaAttribute(key, schema);
|
|
423
449
|
|
|
450
|
+
if (!validKey) {
|
|
451
|
+
removeOperator(key);
|
|
452
|
+
}
|
|
424
453
|
// Handle attributes
|
|
425
|
-
if (attribute) {
|
|
454
|
+
else if (attribute) {
|
|
426
455
|
// Relations
|
|
427
456
|
if (attribute.type === 'relation') {
|
|
428
457
|
filters[key] = convertAndSanitizeFilters(value, strapi.getModel(attribute.target));
|
package/lib/index.js
CHANGED
|
@@ -28,7 +28,6 @@ const { removeUndefined, keysDeep } = require('./object-formatting');
|
|
|
28
28
|
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
|
|
29
29
|
const { generateTimestampCode } = require('./code-generator');
|
|
30
30
|
const contentTypes = require('./content-types');
|
|
31
|
-
const webhook = require('./webhook');
|
|
32
31
|
const env = require('./env-helper');
|
|
33
32
|
const relations = require('./relations');
|
|
34
33
|
const setCreatorFields = require('./set-creator-fields');
|
|
@@ -43,6 +42,7 @@ const importDefault = require('./import-default');
|
|
|
43
42
|
const template = require('./template');
|
|
44
43
|
const file = require('./file');
|
|
45
44
|
const traverse = require('./traverse');
|
|
45
|
+
const { isOperator, isOperatorOfType } = require('./operators');
|
|
46
46
|
|
|
47
47
|
module.exports = {
|
|
48
48
|
yup,
|
|
@@ -75,7 +75,6 @@ module.exports = {
|
|
|
75
75
|
isCamelCase,
|
|
76
76
|
toKebabCase,
|
|
77
77
|
contentTypes,
|
|
78
|
-
webhook,
|
|
79
78
|
env,
|
|
80
79
|
relations,
|
|
81
80
|
setCreatorFields,
|
|
@@ -93,4 +92,6 @@ module.exports = {
|
|
|
93
92
|
importDefault,
|
|
94
93
|
file,
|
|
95
94
|
traverse,
|
|
95
|
+
isOperator,
|
|
96
|
+
isOperatorOfType,
|
|
96
97
|
};
|
package/lib/operators.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const GROUP_OPERATORS = ['$and', '$or'];
|
|
4
|
+
|
|
5
|
+
const WHERE_OPERATORS = [
|
|
6
|
+
'$not',
|
|
7
|
+
'$in',
|
|
8
|
+
'$notIn',
|
|
9
|
+
'$eq',
|
|
10
|
+
'$eqi',
|
|
11
|
+
'$ne',
|
|
12
|
+
'$gt',
|
|
13
|
+
'$gte',
|
|
14
|
+
'$lt',
|
|
15
|
+
'$lte',
|
|
16
|
+
'$null',
|
|
17
|
+
'$notNull',
|
|
18
|
+
'$between',
|
|
19
|
+
'$startsWith',
|
|
20
|
+
'$endsWith',
|
|
21
|
+
'$startsWithi',
|
|
22
|
+
'$endsWithi',
|
|
23
|
+
'$contains',
|
|
24
|
+
'$notContains',
|
|
25
|
+
'$containsi',
|
|
26
|
+
'$notContainsi',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CAST_OPERATORS = [
|
|
30
|
+
'$not',
|
|
31
|
+
'$in',
|
|
32
|
+
'$notIn',
|
|
33
|
+
'$eq',
|
|
34
|
+
'$ne',
|
|
35
|
+
'$gt',
|
|
36
|
+
'$gte',
|
|
37
|
+
'$lt',
|
|
38
|
+
'$lte',
|
|
39
|
+
'$between',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const ARRAY_OPERATORS = ['$in', '$notIn', '$between'];
|
|
43
|
+
|
|
44
|
+
const OPERATORS = {
|
|
45
|
+
where: WHERE_OPERATORS,
|
|
46
|
+
cast: CAST_OPERATORS,
|
|
47
|
+
group: GROUP_OPERATORS,
|
|
48
|
+
array: ARRAY_OPERATORS,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// for performance, cache all operators in lowercase
|
|
52
|
+
const OPERATORS_LOWERCASE = Object.fromEntries(
|
|
53
|
+
Object.entries(OPERATORS).map(([key, values]) => [
|
|
54
|
+
key,
|
|
55
|
+
values.map((value) => value.toLowerCase()),
|
|
56
|
+
])
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const isOperatorOfType = (type, key, ignoreCase = false) => {
|
|
60
|
+
if (ignoreCase) {
|
|
61
|
+
return OPERATORS_LOWERCASE[type]?.includes(key.toLowerCase()) ?? false;
|
|
62
|
+
}
|
|
63
|
+
return OPERATORS[type]?.includes(key) ?? false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const isOperator = (key, ignoreCase = false) => {
|
|
67
|
+
return Object.keys(OPERATORS).some((type) => isOperatorOfType(type, key, ignoreCase));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
isOperator,
|
|
72
|
+
isOperatorOfType,
|
|
73
|
+
OPERATORS,
|
|
74
|
+
};
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
removeDynamicZones,
|
|
20
20
|
removeMorphToRelations,
|
|
21
21
|
} = require('./visitors');
|
|
22
|
+
const { isOperator } = require('../operators');
|
|
22
23
|
|
|
23
24
|
const sanitizePasswords = (schema) => async (entity) => {
|
|
24
25
|
return traverseEntity(removePassword, { schema }, entity);
|
|
@@ -37,6 +38,17 @@ const defaultSanitizeOutput = async (schema, entity) => {
|
|
|
37
38
|
|
|
38
39
|
const defaultSanitizeFilters = curry((schema, filters) => {
|
|
39
40
|
return pipeAsync(
|
|
41
|
+
// Remove keys that are not attributes or valid operators
|
|
42
|
+
traverseQueryFilters(
|
|
43
|
+
({ key, attribute }, { remove }) => {
|
|
44
|
+
const isAttribute = !!attribute;
|
|
45
|
+
|
|
46
|
+
if (!isAttribute && !isOperator(key) && key !== 'id') {
|
|
47
|
+
remove(key);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{ schema }
|
|
51
|
+
),
|
|
40
52
|
// Remove dynamic zones from filters
|
|
41
53
|
traverseQueryFilters(removeDynamicZones, { schema }),
|
|
42
54
|
// Remove morpTo relations from filters
|
|
@@ -7,7 +7,7 @@ module.exports = ({ schema, key, attribute }, { remove }) => {
|
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const isPrivate = isPrivateAttribute(schema, key)
|
|
10
|
+
const isPrivate = attribute.private === true || isPrivateAttribute(schema, key);
|
|
11
11
|
|
|
12
12
|
if (isPrivate) {
|
|
13
13
|
remove(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/utils",
|
|
3
|
-
"version": "4.11.0-
|
|
3
|
+
"version": "4.11.0-exp.9xg4-3qfm-9w8f.1",
|
|
4
4
|
"description": "Shared utilities for the Strapi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"node": ">=14.19.1 <=18.x.x",
|
|
50
50
|
"npm": ">=6.0.0"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "54c4fa25b2706612f85aaf103f54c071c281f23b"
|
|
53
53
|
}
|
package/lib/webhook.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const webhookEvents = {
|
|
4
|
-
ENTRY_CREATE: 'entry.create',
|
|
5
|
-
ENTRY_UPDATE: 'entry.update',
|
|
6
|
-
ENTRY_DELETE: 'entry.delete',
|
|
7
|
-
ENTRY_PUBLISH: 'entry.publish',
|
|
8
|
-
ENTRY_UNPUBLISH: 'entry.unpublish',
|
|
9
|
-
MEDIA_CREATE: 'media.create',
|
|
10
|
-
MEDIA_UPDATE: 'media.update',
|
|
11
|
-
MEDIA_DELETE: 'media.delete',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
module.exports = {
|
|
15
|
-
webhookEvents,
|
|
16
|
-
};
|