@strapi/utils 4.9.0-exp.90df253ba90fd6879eb56a720a1f80d04ff745b8 → 4.9.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/.eslintignore +2 -0
- package/.eslintrc.js +4 -0
- package/lib/async.js +2 -3
- package/lib/errors.js +9 -0
- package/lib/sanitize/index.js +8 -4
- package/lib/sanitize/sanitizers.js +18 -10
- package/lib/traverse/factory.js +2 -1
- package/lib/traverse/query-populate.js +9 -3
- package/lib/traverse-entity.js +83 -40
- package/package.json +5 -3
package/.eslintignore
ADDED
package/.eslintrc.js
ADDED
package/lib/async.js
CHANGED
|
@@ -6,9 +6,8 @@ const { curry, curryN } = require('lodash/fp');
|
|
|
6
6
|
function pipeAsync(...methods) {
|
|
7
7
|
return async (data) => {
|
|
8
8
|
let res = data;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
res = await method(res);
|
|
9
|
+
for (let i = 0; i < methods.length; i += 1) {
|
|
10
|
+
res = await methods[i](res);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
return res;
|
package/lib/errors.js
CHANGED
|
@@ -89,6 +89,14 @@ class PolicyError extends ForbiddenError {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
class NotImplementedError extends ApplicationError {
|
|
93
|
+
constructor(message, details) {
|
|
94
|
+
super(message, details);
|
|
95
|
+
this.name = 'NotImplementedError';
|
|
96
|
+
this.message = message || 'This feature is not implemented yet';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
module.exports = {
|
|
93
101
|
HttpError,
|
|
94
102
|
ApplicationError,
|
|
@@ -101,4 +109,5 @@ module.exports = {
|
|
|
101
109
|
RateLimitError,
|
|
102
110
|
PayloadTooLargeError,
|
|
103
111
|
PolicyError,
|
|
112
|
+
NotImplementedError,
|
|
104
113
|
};
|
package/lib/sanitize/index.js
CHANGED
|
@@ -37,12 +37,16 @@ const createContentAPISanitizers = () => {
|
|
|
37
37
|
return pipeAsync(...transforms)(data);
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const sanitizeOutput = async (data, schema, { auth } = {}) => {
|
|
41
41
|
if (isArray(data)) {
|
|
42
|
-
|
|
42
|
+
const res = new Array(data.length);
|
|
43
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
44
|
+
res[i] = await sanitizeOutput(data[i], schema, { auth });
|
|
45
|
+
}
|
|
46
|
+
return res;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
const transforms = [sanitizers.defaultSanitizeOutput(schema)];
|
|
49
|
+
const transforms = [(data) => sanitizers.defaultSanitizeOutput(schema, data)];
|
|
46
50
|
|
|
47
51
|
if (auth) {
|
|
48
52
|
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
@@ -122,7 +126,7 @@ const createContentAPISanitizers = () => {
|
|
|
122
126
|
|
|
123
127
|
return {
|
|
124
128
|
input: sanitizeInput,
|
|
125
|
-
output:
|
|
129
|
+
output: sanitizeOutput,
|
|
126
130
|
query: sanitizeQuery,
|
|
127
131
|
filters: sanitizeFilters,
|
|
128
132
|
sort: sanitizeSort,
|
|
@@ -20,17 +20,20 @@ const {
|
|
|
20
20
|
removeMorphToRelations,
|
|
21
21
|
} = require('./visitors');
|
|
22
22
|
|
|
23
|
-
const sanitizePasswords =
|
|
23
|
+
const sanitizePasswords = (schema) => async (entity) => {
|
|
24
24
|
return traverseEntity(removePassword, { schema }, entity);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const sanitizePrivates = curry((schema, entity) => {
|
|
28
|
-
return traverseEntity(removePrivate, { schema }, entity);
|
|
29
|
-
});
|
|
25
|
+
};
|
|
30
26
|
|
|
31
|
-
const defaultSanitizeOutput =
|
|
32
|
-
return
|
|
33
|
-
|
|
27
|
+
const defaultSanitizeOutput = async (schema, entity) => {
|
|
28
|
+
return traverseEntity(
|
|
29
|
+
(...args) => {
|
|
30
|
+
removePassword(...args);
|
|
31
|
+
removePrivate(...args);
|
|
32
|
+
},
|
|
33
|
+
{ schema },
|
|
34
|
+
entity
|
|
35
|
+
);
|
|
36
|
+
};
|
|
34
37
|
|
|
35
38
|
const defaultSanitizeFilters = curry((schema, filters) => {
|
|
36
39
|
return pipeAsync(
|
|
@@ -59,6 +62,12 @@ const defaultSanitizeSort = curry((schema, sort) => {
|
|
|
59
62
|
// Remove non attribute keys
|
|
60
63
|
traverseQuerySort(
|
|
61
64
|
({ key, attribute }, { remove }) => {
|
|
65
|
+
// ID is not an attribute per se, so we need to make
|
|
66
|
+
// an extra check to ensure we're not removing it
|
|
67
|
+
if (key === 'id') {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
if (!attribute) {
|
|
63
72
|
remove(key);
|
|
64
73
|
}
|
|
@@ -134,7 +143,6 @@ const defaultSanitizePopulate = curry((schema, populate) => {
|
|
|
134
143
|
|
|
135
144
|
module.exports = {
|
|
136
145
|
sanitizePasswords,
|
|
137
|
-
sanitizePrivates,
|
|
138
146
|
defaultSanitizeOutput,
|
|
139
147
|
defaultSanitizeFilters,
|
|
140
148
|
defaultSanitizeSort,
|
package/lib/traverse/factory.js
CHANGED
|
@@ -39,7 +39,8 @@ module.exports = () => {
|
|
|
39
39
|
for (const key of keys) {
|
|
40
40
|
const attribute =
|
|
41
41
|
schema?.attributes?.[key] ??
|
|
42
|
-
//
|
|
42
|
+
// FIX: Needed to not break existing behavior on the API.
|
|
43
|
+
// It looks for the attribute in the DB metadata when the key is in snake_case
|
|
43
44
|
schema?.attributes?.[strapi.db.metadata.get(schema?.uid).columnToAttribute[key]];
|
|
44
45
|
|
|
45
46
|
const newPath = { ...path };
|
|
@@ -160,6 +160,8 @@ const populate = traverseFactory()
|
|
|
160
160
|
const { components } = attribute;
|
|
161
161
|
const { on, ...properties } = value;
|
|
162
162
|
|
|
163
|
+
const newValue = {};
|
|
164
|
+
|
|
163
165
|
// Handle legacy DZ params
|
|
164
166
|
let newProperties = properties;
|
|
165
167
|
|
|
@@ -168,11 +170,15 @@ const populate = traverseFactory()
|
|
|
168
170
|
newProperties = await recurse(visitor, { schema: componentSchema, path }, newProperties);
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
Object.assign(newValue, newProperties);
|
|
174
|
+
|
|
171
175
|
// Handle new morph fragment syntax
|
|
172
|
-
|
|
176
|
+
if (on) {
|
|
177
|
+
const newOn = await recurse(visitor, { schema, path }, { on });
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
// Recompose both syntaxes
|
|
180
|
+
Object.assign(newValue, newOn);
|
|
181
|
+
}
|
|
176
182
|
|
|
177
183
|
set(key, newValue);
|
|
178
184
|
} else {
|
package/lib/traverse-entity.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { clone, isObject, isArray, isNil, curry } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseMorphRelationTarget = async (visitor, path, entry) => {
|
|
6
|
+
const targetSchema = strapi.getModel(entry.__type);
|
|
7
|
+
|
|
8
|
+
const traverseOptions = { schema: targetSchema, path };
|
|
9
|
+
|
|
10
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const traverseRelationTarget = (schema) => async (visitor, path, entry) => {
|
|
14
|
+
const traverseOptions = { schema, path };
|
|
15
|
+
|
|
16
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const traverseMediaTarget = async (visitor, path, entry) => {
|
|
20
|
+
const targetSchemaUID = 'plugin::upload.file';
|
|
21
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
22
|
+
|
|
23
|
+
const traverseOptions = { schema: targetSchema, path };
|
|
24
|
+
|
|
25
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const traverseComponent = async (visitor, path, schema, entry) => {
|
|
29
|
+
const traverseOptions = { schema, path };
|
|
30
|
+
|
|
31
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const visitDynamicZoneEntry = async (visitor, path, entry) => {
|
|
35
|
+
const targetSchema = strapi.getModel(entry.__component);
|
|
36
|
+
const traverseOptions = { schema: targetSchema, path };
|
|
37
|
+
|
|
38
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
39
|
+
};
|
|
4
40
|
|
|
5
41
|
const traverseEntity = async (visitor, options, entity) => {
|
|
6
42
|
const { path = { raw: null, attribute: null }, schema } = options;
|
|
@@ -11,9 +47,13 @@ const traverseEntity = async (visitor, options, entity) => {
|
|
|
11
47
|
}
|
|
12
48
|
|
|
13
49
|
// Don't mutate the original entity object
|
|
14
|
-
|
|
50
|
+
// only clone at 1st level as the next level will get clone when traversed
|
|
51
|
+
const copy = clone(entity);
|
|
52
|
+
const visitorUtils = createVisitorUtils({ data: copy });
|
|
15
53
|
|
|
16
|
-
|
|
54
|
+
const keys = Object.keys(copy);
|
|
55
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
56
|
+
const key = keys[i];
|
|
17
57
|
// Retrieve the attribute definition associated to the key from the schema
|
|
18
58
|
const attribute = schema.attributes[key];
|
|
19
59
|
|
|
@@ -32,7 +72,6 @@ const traverseEntity = async (visitor, options, entity) => {
|
|
|
32
72
|
|
|
33
73
|
// Visit the current attribute
|
|
34
74
|
const visitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath };
|
|
35
|
-
const visitorUtils = createVisitorUtils({ data: copy });
|
|
36
75
|
|
|
37
76
|
await visitor(visitorOptions, visitorUtils);
|
|
38
77
|
|
|
@@ -52,58 +91,62 @@ const traverseEntity = async (visitor, options, entity) => {
|
|
|
52
91
|
if (isRelation) {
|
|
53
92
|
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
54
93
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
59
|
-
|
|
60
|
-
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
94
|
+
const method = isMorphRelation
|
|
95
|
+
? traverseMorphRelationTarget
|
|
96
|
+
: traverseRelationTarget(strapi.getModel(attribute.target));
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
if (isArray(value)) {
|
|
99
|
+
const res = new Array(value.length);
|
|
100
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
101
|
+
res[i] = await method(visitor, newPath, value[i]);
|
|
102
|
+
}
|
|
103
|
+
copy[key] = res;
|
|
104
|
+
} else {
|
|
105
|
+
copy[key] = await method(visitor, newPath, value);
|
|
106
|
+
}
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
copy[key] = isArray(value)
|
|
67
|
-
? await Promise.all(value.map(traverseTarget))
|
|
68
|
-
: await traverseTarget(value);
|
|
108
|
+
continue;
|
|
69
109
|
}
|
|
70
110
|
|
|
71
111
|
if (isMedia) {
|
|
72
|
-
const traverseTarget = (entry) => {
|
|
73
|
-
const targetSchemaUID = 'plugin::upload.file';
|
|
74
|
-
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
75
|
-
|
|
76
|
-
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
77
|
-
|
|
78
|
-
return traverseEntity(visitor, traverseOptions, entry);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
112
|
// need to update copy
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
if (isArray(value)) {
|
|
114
|
+
const res = new Array(value.length);
|
|
115
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
116
|
+
res[i] = await traverseMediaTarget(visitor, newPath, value[i]);
|
|
117
|
+
}
|
|
118
|
+
copy[key] = res;
|
|
119
|
+
} else {
|
|
120
|
+
copy[key] = await traverseMediaTarget(visitor, newPath, value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
continue;
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
if (isComponent) {
|
|
88
127
|
const targetSchema = strapi.getModel(attribute.component);
|
|
89
|
-
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
90
128
|
|
|
91
|
-
|
|
129
|
+
if (isArray(value)) {
|
|
130
|
+
const res = new Array(value.length);
|
|
131
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
132
|
+
res[i] = await traverseComponent(visitor, newPath, targetSchema, value[i]);
|
|
133
|
+
}
|
|
134
|
+
copy[key] = res;
|
|
135
|
+
} else {
|
|
136
|
+
copy[key] = await traverseComponent(visitor, newPath, targetSchema, value);
|
|
137
|
+
}
|
|
92
138
|
|
|
93
|
-
|
|
94
|
-
? await Promise.all(value.map(traverseComponent))
|
|
95
|
-
: await traverseComponent(value);
|
|
139
|
+
continue;
|
|
96
140
|
}
|
|
97
141
|
|
|
98
142
|
if (isDynamicZone && isArray(value)) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
};
|
|
143
|
+
const res = new Array(value.length);
|
|
144
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
145
|
+
res[i] = await visitDynamicZoneEntry(visitor, newPath, value[i]);
|
|
146
|
+
}
|
|
147
|
+
copy[key] = res;
|
|
105
148
|
|
|
106
|
-
|
|
149
|
+
continue;
|
|
107
150
|
}
|
|
108
151
|
}
|
|
109
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/utils",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.1",
|
|
4
4
|
"description": "Shared utilities for the Strapi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"lib": "./lib"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test:unit": "jest
|
|
35
|
+
"test:unit": "run -T jest",
|
|
36
|
+
"test:unit:watch": "run -T jest --watch",
|
|
37
|
+
"lint": "run -T eslint ."
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@sindresorhus/slugify": "1.1.0",
|
|
@@ -46,5 +48,5 @@
|
|
|
46
48
|
"node": ">=14.19.1 <=18.x.x",
|
|
47
49
|
"npm": ">=6.0.0"
|
|
48
50
|
},
|
|
49
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "c8f2f0543b45dda8eadde21a0782b64d156fc786"
|
|
50
52
|
}
|