@strapi/utils 4.9.0-alpha.0 → 4.9.0-beta.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 +2 -2
- package/lib/convert-query-params.js +3 -2
- package/lib/env-helper.js +20 -0
- package/lib/index.js +2 -0
- package/lib/sanitize/index.js +125 -47
- package/lib/sanitize/sanitizers.js +125 -2
- package/lib/sanitize/visitors/allowed-fields.js +6 -2
- package/lib/sanitize/visitors/index.js +2 -0
- package/lib/sanitize/visitors/remove-dynamic-zones.js +9 -0
- package/lib/sanitize/visitors/remove-morph-to-relations.js +9 -0
- package/lib/sanitize/visitors/remove-password.js +1 -1
- package/lib/sanitize/visitors/remove-private.js +6 -2
- package/lib/sanitize/visitors/remove-restricted-relations.js +4 -0
- package/lib/sanitize/visitors/restricted-fields.js +1 -1
- package/lib/traverse/factory.js +157 -0
- package/lib/traverse/index.js +16 -0
- package/lib/traverse/query-fields.js +39 -0
- package/lib/traverse/query-filters.js +97 -0
- package/lib/traverse/query-populate.js +191 -0
- package/lib/traverse/query-sort.js +171 -0
- package/lib/traverse-entity.js +8 -2
- package/package.json +2 -2
package/lib/content-types.js
CHANGED
|
@@ -99,8 +99,8 @@ const getPrivateAttributes = (model = {}) => {
|
|
|
99
99
|
);
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
const isPrivateAttribute = (model
|
|
103
|
-
return model
|
|
102
|
+
const isPrivateAttribute = (model, attributeName) => {
|
|
103
|
+
return model?.privateAttributes?.includes(attributeName) ?? false;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
const isScalarAttribute = (attribute) => {
|
|
@@ -232,7 +232,8 @@ const convertPopulateObject = (populate, schema) => {
|
|
|
232
232
|
isMediaAttribute(attribute) ||
|
|
233
233
|
isMorphToRelationalAttribute(attribute);
|
|
234
234
|
|
|
235
|
-
const hasFragmentPopulateDefined =
|
|
235
|
+
const hasFragmentPopulateDefined =
|
|
236
|
+
typeof subPopulate === 'object' && 'on' in subPopulate && !isNil(subPopulate.on);
|
|
236
237
|
|
|
237
238
|
if (isAllowedAttributeForFragmentPopulate && hasFragmentPopulateDefined) {
|
|
238
239
|
return {
|
|
@@ -418,7 +419,7 @@ const convertAndSanitizeFilters = (filters, schema) => {
|
|
|
418
419
|
|
|
419
420
|
// Here, `key` can either be an operator or an attribute name
|
|
420
421
|
for (const [key, value] of Object.entries(filters)) {
|
|
421
|
-
const attribute = get(key, schema
|
|
422
|
+
const attribute = get(key, schema?.attributes);
|
|
422
423
|
|
|
423
424
|
// Handle attributes
|
|
424
425
|
if (attribute) {
|
package/lib/env-helper.js
CHANGED
|
@@ -71,6 +71,26 @@ const utils = {
|
|
|
71
71
|
const value = process.env[key];
|
|
72
72
|
return new Date(value);
|
|
73
73
|
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets a value from env that matches oneOf provided values
|
|
77
|
+
* @param {string} key
|
|
78
|
+
* @param {string[]} expectedValues
|
|
79
|
+
* @param {string|undefined} defaultValue
|
|
80
|
+
* @returns {string|undefined}
|
|
81
|
+
*/
|
|
82
|
+
oneOf(key, expectedValues, defaultValue) {
|
|
83
|
+
if (!expectedValues) {
|
|
84
|
+
throw new Error(`env.oneOf requires expectedValues`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (defaultValue && !expectedValues.includes(defaultValue)) {
|
|
88
|
+
throw new Error(`env.oneOf requires defaultValue to be included in expectedValues`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rawValue = env(key, defaultValue);
|
|
92
|
+
return expectedValues.includes(rawValue) ? rawValue : defaultValue;
|
|
93
|
+
},
|
|
74
94
|
};
|
|
75
95
|
|
|
76
96
|
Object.assign(env, utils);
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const convertQueryParams = require('./convert-query-params');
|
|
|
42
42
|
const importDefault = require('./import-default');
|
|
43
43
|
const template = require('./template');
|
|
44
44
|
const file = require('./file');
|
|
45
|
+
const traverse = require('./traverse');
|
|
45
46
|
|
|
46
47
|
module.exports = {
|
|
47
48
|
yup,
|
|
@@ -91,4 +92,5 @@ module.exports = {
|
|
|
91
92
|
convertQueryParams,
|
|
92
93
|
importDefault,
|
|
93
94
|
file,
|
|
95
|
+
traverse,
|
|
94
96
|
};
|
package/lib/sanitize/index.js
CHANGED
|
@@ -1,60 +1,138 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isArray } = require('lodash/fp');
|
|
3
|
+
const { isArray, cloneDeep } = require('lodash/fp');
|
|
4
4
|
|
|
5
|
-
const traverseEntity = require('../traverse-entity');
|
|
6
5
|
const { getNonWritableAttributes } = require('../content-types');
|
|
7
6
|
const { pipeAsync } = require('../async');
|
|
8
7
|
|
|
9
8
|
const visitors = require('./visitors');
|
|
10
9
|
const sanitizers = require('./sanitizers');
|
|
10
|
+
const traverseEntity = require('../traverse-entity');
|
|
11
|
+
|
|
12
|
+
const { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } = require('../traverse');
|
|
13
|
+
|
|
14
|
+
const createContentAPISanitizers = () => {
|
|
15
|
+
const sanitizeInput = (data, schema, { auth } = {}) => {
|
|
16
|
+
if (isArray(data)) {
|
|
17
|
+
return Promise.all(data.map((entry) => sanitizeInput(entry, schema, { auth })));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const nonWritableAttributes = getNonWritableAttributes(schema);
|
|
21
|
+
|
|
22
|
+
const transforms = [
|
|
23
|
+
// Remove non writable attributes
|
|
24
|
+
traverseEntity(visitors.restrictedFields(nonWritableAttributes), { schema }),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (auth) {
|
|
28
|
+
// Remove restricted relations
|
|
29
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Apply sanitizers from registry if exists
|
|
33
|
+
strapi.sanitizers
|
|
34
|
+
.get('content-api.input')
|
|
35
|
+
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
|
36
|
+
|
|
37
|
+
return pipeAsync(...transforms)(data);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const sanitizeOuput = (data, schema, { auth } = {}) => {
|
|
41
|
+
if (isArray(data)) {
|
|
42
|
+
return Promise.all(data.map((entry) => sanitizeOuput(entry, schema, { auth })));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const transforms = [sanitizers.defaultSanitizeOutput(schema)];
|
|
46
|
+
|
|
47
|
+
if (auth) {
|
|
48
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apply sanitizers from registry if exists
|
|
52
|
+
strapi.sanitizers
|
|
53
|
+
.get('content-api.output')
|
|
54
|
+
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
|
55
|
+
|
|
56
|
+
return pipeAsync(...transforms)(data);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const sanitizeQuery = async (query, schema, { auth } = {}) => {
|
|
60
|
+
const { filters, sort, fields, populate } = query;
|
|
61
|
+
|
|
62
|
+
const sanitizedQuery = cloneDeep(query);
|
|
63
|
+
|
|
64
|
+
if (filters) {
|
|
65
|
+
Object.assign(sanitizedQuery, { filters: await sanitizeFilters(filters, schema, { auth }) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sort) {
|
|
69
|
+
Object.assign(sanitizedQuery, { sort: await sanitizeSort(sort, schema, { auth }) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (fields) {
|
|
73
|
+
Object.assign(sanitizedQuery, { fields: await sanitizeFields(fields, schema) });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (populate) {
|
|
77
|
+
Object.assign(sanitizedQuery, { populate: await sanitizePopulate(populate, schema) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return sanitizedQuery;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const sanitizeFilters = (filters, schema, { auth } = {}) => {
|
|
84
|
+
if (isArray(filters)) {
|
|
85
|
+
return Promise.all(filters.map((filter) => sanitizeFilters(filter, schema, { auth })));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const transforms = [sanitizers.defaultSanitizeFilters(schema)];
|
|
89
|
+
|
|
90
|
+
if (auth) {
|
|
91
|
+
transforms.push(traverseQueryFilters(visitors.removeRestrictedRelations(auth), { schema }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return pipeAsync(...transforms)(filters);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const sanitizeSort = (sort, schema, { auth } = {}) => {
|
|
98
|
+
const transforms = [sanitizers.defaultSanitizeSort(schema)];
|
|
99
|
+
|
|
100
|
+
if (auth) {
|
|
101
|
+
transforms.push(traverseQuerySort(visitors.removeRestrictedRelations(auth), { schema }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return pipeAsync(...transforms)(sort);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sanitizeFields = (fields, schema) => {
|
|
108
|
+
const transforms = [sanitizers.defaultSanitizeFields(schema)];
|
|
109
|
+
|
|
110
|
+
return pipeAsync(...transforms)(fields);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const sanitizePopulate = (populate, schema, { auth } = {}) => {
|
|
114
|
+
const transforms = [sanitizers.defaultSanitizePopulate(schema)];
|
|
115
|
+
|
|
116
|
+
if (auth) {
|
|
117
|
+
transforms.push(traverseQueryPopulate(visitors.removeRestrictedRelations(auth), { schema }));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return pipeAsync(...transforms)(populate);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
input: sanitizeInput,
|
|
125
|
+
output: sanitizeOuput,
|
|
126
|
+
query: sanitizeQuery,
|
|
127
|
+
filters: sanitizeFilters,
|
|
128
|
+
sort: sanitizeSort,
|
|
129
|
+
fields: sanitizeFields,
|
|
130
|
+
populate: sanitizePopulate,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
11
133
|
|
|
12
134
|
module.exports = {
|
|
13
|
-
contentAPI:
|
|
14
|
-
input(data, schema, { auth } = {}) {
|
|
15
|
-
if (isArray(data)) {
|
|
16
|
-
return Promise.all(data.map((entry) => this.input(entry, schema, { auth })));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const nonWritableAttributes = getNonWritableAttributes(schema);
|
|
20
|
-
|
|
21
|
-
const transforms = [
|
|
22
|
-
// Remove non writable attributes
|
|
23
|
-
traverseEntity(visitors.restrictedFields(nonWritableAttributes), { schema }),
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
if (auth) {
|
|
27
|
-
// Remove restricted relations
|
|
28
|
-
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Apply sanitizers from registry if exists
|
|
32
|
-
strapi.sanitizers
|
|
33
|
-
.get('content-api.input')
|
|
34
|
-
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
|
35
|
-
|
|
36
|
-
return pipeAsync(...transforms)(data);
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
output(data, schema, { auth } = {}) {
|
|
40
|
-
if (isArray(data)) {
|
|
41
|
-
return Promise.all(data.map((entry) => this.output(entry, schema, { auth })));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const transforms = [sanitizers.defaultSanitizeOutput(schema)];
|
|
45
|
-
|
|
46
|
-
if (auth) {
|
|
47
|
-
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Apply sanitizers from registry if exists
|
|
51
|
-
strapi.sanitizers
|
|
52
|
-
.get('content-api.output')
|
|
53
|
-
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
|
54
|
-
|
|
55
|
-
return pipeAsync(...transforms)(data);
|
|
56
|
-
},
|
|
57
|
-
},
|
|
135
|
+
contentAPI: createContentAPISanitizers(),
|
|
58
136
|
|
|
59
137
|
sanitizers,
|
|
60
138
|
visitors,
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { curry } = require('lodash/fp');
|
|
3
|
+
const { curry, isEmpty, isNil, isArray, isObject } = require('lodash/fp');
|
|
4
4
|
|
|
5
5
|
const { pipeAsync } = require('../async');
|
|
6
6
|
const traverseEntity = require('../traverse-entity');
|
|
7
|
+
const { isScalarAttribute } = require('../content-types');
|
|
7
8
|
|
|
8
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
traverseQueryFilters,
|
|
11
|
+
traverseQuerySort,
|
|
12
|
+
traverseQueryPopulate,
|
|
13
|
+
traverseQueryFields,
|
|
14
|
+
} = require('../traverse');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
removePassword,
|
|
18
|
+
removePrivate,
|
|
19
|
+
removeDynamicZones,
|
|
20
|
+
removeMorphToRelations,
|
|
21
|
+
} = require('./visitors');
|
|
9
22
|
|
|
10
23
|
const sanitizePasswords = curry((schema, entity) => {
|
|
11
24
|
return traverseEntity(removePassword, { schema }, entity);
|
|
@@ -19,8 +32,118 @@ const defaultSanitizeOutput = curry((schema, entity) => {
|
|
|
19
32
|
return pipeAsync(sanitizePrivates(schema), sanitizePasswords(schema))(entity);
|
|
20
33
|
});
|
|
21
34
|
|
|
35
|
+
const defaultSanitizeFilters = curry((schema, filters) => {
|
|
36
|
+
return pipeAsync(
|
|
37
|
+
// Remove dynamic zones from filters
|
|
38
|
+
traverseQueryFilters(removeDynamicZones, { schema }),
|
|
39
|
+
// Remove morpTo relations from filters
|
|
40
|
+
traverseQueryFilters(removeMorphToRelations, { schema }),
|
|
41
|
+
// Remove passwords from filters
|
|
42
|
+
traverseQueryFilters(removePassword, { schema }),
|
|
43
|
+
// Remove private from filters
|
|
44
|
+
traverseQueryFilters(removePrivate, { schema }),
|
|
45
|
+
// Remove empty objects
|
|
46
|
+
traverseQueryFilters(
|
|
47
|
+
({ key, value }, { remove }) => {
|
|
48
|
+
if (isObject(value) && isEmpty(value)) {
|
|
49
|
+
remove(key);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{ schema }
|
|
53
|
+
)
|
|
54
|
+
)(filters);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const defaultSanitizeSort = curry((schema, sort) => {
|
|
58
|
+
return pipeAsync(
|
|
59
|
+
// Remove non attribute keys
|
|
60
|
+
traverseQuerySort(
|
|
61
|
+
({ key, attribute }, { remove }) => {
|
|
62
|
+
// ID is not an attribute per se, so we need to make
|
|
63
|
+
// an extra check to ensure we're not removing it
|
|
64
|
+
if (key === 'id') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!attribute) {
|
|
69
|
+
remove(key);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{ schema }
|
|
73
|
+
),
|
|
74
|
+
// Remove dynamic zones from sort
|
|
75
|
+
traverseQuerySort(removeDynamicZones, { schema }),
|
|
76
|
+
// Remove morpTo relations from sort
|
|
77
|
+
traverseQuerySort(removeMorphToRelations, { schema }),
|
|
78
|
+
// Remove private from sort
|
|
79
|
+
traverseQuerySort(removePrivate, { schema }),
|
|
80
|
+
// Remove passwords from filters
|
|
81
|
+
traverseQuerySort(removePassword, { schema }),
|
|
82
|
+
// Remove keys for empty non-scalar values
|
|
83
|
+
traverseQuerySort(
|
|
84
|
+
({ key, attribute, value }, { remove }) => {
|
|
85
|
+
if (!isScalarAttribute(attribute) && isEmpty(value)) {
|
|
86
|
+
remove(key);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{ schema }
|
|
90
|
+
)
|
|
91
|
+
)(sort);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const defaultSanitizeFields = curry((schema, fields) => {
|
|
95
|
+
return pipeAsync(
|
|
96
|
+
// Only keep scalar attributes
|
|
97
|
+
traverseQueryFields(
|
|
98
|
+
({ key, attribute }, { remove }) => {
|
|
99
|
+
if (isNil(attribute) || !isScalarAttribute(attribute)) {
|
|
100
|
+
remove(key);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{ schema }
|
|
104
|
+
),
|
|
105
|
+
// Remove private fields
|
|
106
|
+
traverseQueryFields(removePrivate, { schema }),
|
|
107
|
+
// Remove password fields
|
|
108
|
+
traverseQueryFields(removePassword, { schema }),
|
|
109
|
+
// Remove nil values from fields array
|
|
110
|
+
(value) => (isArray(value) ? value.filter((field) => !isNil(field)) : value)
|
|
111
|
+
)(fields);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const defaultSanitizePopulate = curry((schema, populate) => {
|
|
115
|
+
return pipeAsync(
|
|
116
|
+
traverseQueryPopulate(
|
|
117
|
+
async ({ key, value, schema, attribute }, { set }) => {
|
|
118
|
+
if (attribute) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (key === 'sort') {
|
|
123
|
+
set(key, await defaultSanitizeSort(schema, value));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (key === 'filters') {
|
|
127
|
+
set(key, await defaultSanitizeFilters(schema, value));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (key === 'fields') {
|
|
131
|
+
set(key, await defaultSanitizeFields(schema, value));
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ schema }
|
|
135
|
+
),
|
|
136
|
+
// Remove private fields
|
|
137
|
+
traverseQueryPopulate(removePrivate, { schema })
|
|
138
|
+
)(populate);
|
|
139
|
+
});
|
|
140
|
+
|
|
22
141
|
module.exports = {
|
|
23
142
|
sanitizePasswords,
|
|
24
143
|
sanitizePrivates,
|
|
25
144
|
defaultSanitizeOutput,
|
|
145
|
+
defaultSanitizeFilters,
|
|
146
|
+
defaultSanitizeSort,
|
|
147
|
+
defaultSanitizeFields,
|
|
148
|
+
defaultSanitizePopulate,
|
|
26
149
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isArray, toPath } = require('lodash/fp');
|
|
3
|
+
const { isArray, isNil, toPath } = require('lodash/fp');
|
|
4
4
|
|
|
5
5
|
module.exports =
|
|
6
6
|
(allowedFields = null) =>
|
|
7
|
-
({ key, path }, { remove }) => {
|
|
7
|
+
({ key, path: { attribute: path } }, { remove }) => {
|
|
8
8
|
// All fields are allowed
|
|
9
9
|
if (allowedFields === null) {
|
|
10
10
|
return;
|
|
@@ -15,6 +15,10 @@ module.exports =
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
if (isNil(path)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
const containedPaths = getContainedPaths(path);
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -4,6 +4,8 @@ module.exports = {
|
|
|
4
4
|
removePassword: require('./remove-password'),
|
|
5
5
|
removePrivate: require('./remove-private'),
|
|
6
6
|
removeRestrictedRelations: require('./remove-restricted-relations'),
|
|
7
|
+
removeMorphToRelations: require('./remove-morph-to-relations'),
|
|
8
|
+
removeDynamicZones: require('./remove-dynamic-zones'),
|
|
7
9
|
allowedFields: require('./allowed-fields'),
|
|
8
10
|
restrictedFields: require('./restricted-fields'),
|
|
9
11
|
};
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { isPrivateAttribute } = require('../../content-types');
|
|
4
4
|
|
|
5
|
-
module.exports = ({ schema, key }, { remove }) => {
|
|
6
|
-
|
|
5
|
+
module.exports = ({ schema, key, attribute }, { remove }) => {
|
|
6
|
+
if (!attribute) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const isPrivate = isPrivateAttribute(schema, key) || attribute.private === true;
|
|
7
11
|
|
|
8
12
|
if (isPrivate) {
|
|
9
13
|
remove(key);
|
|
@@ -7,6 +7,10 @@ const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = require('../../content-ty
|
|
|
7
7
|
module.exports =
|
|
8
8
|
(auth) =>
|
|
9
9
|
async ({ data, key, attribute, schema }, { remove, set }) => {
|
|
10
|
+
if (!attribute) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
const isRelation = attribute.type === 'relation';
|
|
11
15
|
|
|
12
16
|
if (!isRelation) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isNil, pick } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = () => {
|
|
6
|
+
const state = {
|
|
7
|
+
parsers: [],
|
|
8
|
+
interceptors: [],
|
|
9
|
+
ignore: [],
|
|
10
|
+
handlers: {
|
|
11
|
+
attributes: [],
|
|
12
|
+
common: [],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const traverse = async (visitor, options, data) => {
|
|
17
|
+
const { path = { raw: null, attribute: null }, schema } = options ?? {};
|
|
18
|
+
|
|
19
|
+
// interceptors
|
|
20
|
+
for (const { predicate, handler } of state.interceptors) {
|
|
21
|
+
if (predicate(data)) {
|
|
22
|
+
return handler(visitor, options, data, { recurse: traverse });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// parsers
|
|
27
|
+
const parser = state.parsers.find((parser) => parser.predicate(data))?.parser;
|
|
28
|
+
const utils = parser?.(data);
|
|
29
|
+
|
|
30
|
+
// Return the data untouched if we don't know how to traverse it
|
|
31
|
+
if (!utils) {
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// main loop
|
|
36
|
+
let out = utils.transform(data);
|
|
37
|
+
const keys = utils.keys(out);
|
|
38
|
+
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const attribute =
|
|
41
|
+
schema?.attributes?.[key] ??
|
|
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
|
|
44
|
+
schema?.attributes?.[strapi.db.metadata.get(schema?.uid).columnToAttribute[key]];
|
|
45
|
+
|
|
46
|
+
const newPath = { ...path };
|
|
47
|
+
|
|
48
|
+
newPath.raw = isNil(path.raw) ? key : `${path.raw}.${key}`;
|
|
49
|
+
|
|
50
|
+
if (!isNil(attribute)) {
|
|
51
|
+
newPath.attribute = isNil(path.attribute) ? key : `${path.attribute}.${key}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// visitors
|
|
55
|
+
|
|
56
|
+
const visitorOptions = {
|
|
57
|
+
key,
|
|
58
|
+
value: utils.get(key, out),
|
|
59
|
+
attribute,
|
|
60
|
+
schema,
|
|
61
|
+
path: newPath,
|
|
62
|
+
data: out,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const transformUtils = {
|
|
66
|
+
remove(key) {
|
|
67
|
+
out = utils.remove(key, out);
|
|
68
|
+
},
|
|
69
|
+
set(key, value) {
|
|
70
|
+
out = utils.set(key, value, out);
|
|
71
|
+
},
|
|
72
|
+
recurse: traverse,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await visitor(visitorOptions, pick(['remove', 'set'], transformUtils));
|
|
76
|
+
|
|
77
|
+
const value = utils.get(key, out);
|
|
78
|
+
|
|
79
|
+
const createContext = () => ({
|
|
80
|
+
key,
|
|
81
|
+
value,
|
|
82
|
+
attribute,
|
|
83
|
+
schema,
|
|
84
|
+
path: newPath,
|
|
85
|
+
data: out,
|
|
86
|
+
visitor,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ignore
|
|
90
|
+
const ignoreCtx = createContext();
|
|
91
|
+
const shouldIgnore = state.ignore.some((predicate) => predicate(ignoreCtx));
|
|
92
|
+
|
|
93
|
+
if (shouldIgnore) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// handlers
|
|
98
|
+
const handlers = [...state.handlers.common, ...state.handlers.attributes];
|
|
99
|
+
|
|
100
|
+
for await (const handler of handlers) {
|
|
101
|
+
const ctx = createContext();
|
|
102
|
+
const pass = await handler.predicate(ctx);
|
|
103
|
+
|
|
104
|
+
if (pass) {
|
|
105
|
+
await handler.handler(ctx, pick(['recurse', 'set'], transformUtils));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
traverse,
|
|
115
|
+
|
|
116
|
+
intercept(predicate, handler) {
|
|
117
|
+
state.interceptors.push({ predicate, handler });
|
|
118
|
+
return this;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
parse(predicate, parser) {
|
|
122
|
+
state.parsers.push({ predicate, parser });
|
|
123
|
+
return this;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
ignore(predicate) {
|
|
127
|
+
state.ignore.push(predicate);
|
|
128
|
+
return this;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
on(predicate, handler) {
|
|
132
|
+
state.handlers.common.push({ predicate, handler });
|
|
133
|
+
return this;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
onAttribute(predicate, handler) {
|
|
137
|
+
state.handlers.attributes.push({ predicate, handler });
|
|
138
|
+
return this;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
onRelation(handler) {
|
|
142
|
+
return this.onAttribute(({ attribute }) => attribute?.type === 'relation', handler);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onMedia(handler) {
|
|
146
|
+
return this.onAttribute(({ attribute }) => attribute?.type === 'media', handler);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
onComponent(handler) {
|
|
150
|
+
return this.onAttribute(({ attribute }) => attribute?.type === 'component', handler);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
onDynamicZone(handler) {
|
|
154
|
+
return this.onAttribute(({ attribute }) => attribute?.type === 'dynamiczone', handler);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const factory = require('./factory');
|
|
4
|
+
|
|
5
|
+
const traverseQueryFilters = require('./query-filters');
|
|
6
|
+
const traverseQuerySort = require('./query-sort');
|
|
7
|
+
const traverseQueryPopulate = require('./query-populate');
|
|
8
|
+
const traverseQueryFields = require('./query-fields');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
factory,
|
|
12
|
+
traverseQueryFilters,
|
|
13
|
+
traverseQuerySort,
|
|
14
|
+
traverseQueryPopulate,
|
|
15
|
+
traverseQueryFields,
|
|
16
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { curry, isArray, isString, eq, trim, constant } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseFactory = require('./factory');
|
|
6
|
+
|
|
7
|
+
const isStringArray = (value) => isArray(value) && value.every(isString);
|
|
8
|
+
|
|
9
|
+
const fields = traverseFactory()
|
|
10
|
+
// Interecept array of strings
|
|
11
|
+
.intercept(isStringArray, async (visitor, options, fields, { recurse }) => {
|
|
12
|
+
return Promise.all(fields.map((field) => recurse(visitor, options, field)));
|
|
13
|
+
})
|
|
14
|
+
// Return wildcards as is
|
|
15
|
+
.intercept(eq('*'), constant('*'))
|
|
16
|
+
// Parse string values
|
|
17
|
+
// Since we're parsing strings only, each value should be an attribute name (and it's value, undefined),
|
|
18
|
+
// thus it shouldn't be possible to set a new value, and get should return the whole data if key === data
|
|
19
|
+
.parse(isString, () => ({
|
|
20
|
+
transform: trim,
|
|
21
|
+
|
|
22
|
+
remove(key, data) {
|
|
23
|
+
return data === key ? undefined : data;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
set(_key, _value, data) {
|
|
27
|
+
return data;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
keys(data) {
|
|
31
|
+
return [data];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
get(key, data) {
|
|
35
|
+
return key === data ? data : undefined;
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
module.exports = curry(fields.traverse);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { curry, isObject, isEmpty, isArray, isNil, cloneDeep } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseFactory = require('./factory');
|
|
6
|
+
|
|
7
|
+
const filters = traverseFactory()
|
|
8
|
+
.intercept(
|
|
9
|
+
// Intercept filters arrays and apply the traversal to each one individually
|
|
10
|
+
isArray,
|
|
11
|
+
async (visitor, options, filters, { recurse }) => {
|
|
12
|
+
return Promise.all(
|
|
13
|
+
filters.map((filter, i) => {
|
|
14
|
+
// In filters, only operators such as $and, $in, $notIn or $or and implicit operators like [...]
|
|
15
|
+
// can have a value array, thus we can update the raw path but not the attribute one
|
|
16
|
+
const newPath = { ...options.path, raw: `${options.path.raw}[${i}]` };
|
|
17
|
+
|
|
18
|
+
return recurse(visitor, { ...options, path: newPath }, filter);
|
|
19
|
+
})
|
|
20
|
+
// todo: move that to the visitors
|
|
21
|
+
).then((res) => res.filter((val) => !(isObject(val) && isEmpty(val))));
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
.intercept(
|
|
25
|
+
// Ignore non object filters and return the value as-is
|
|
26
|
+
(filters) => !isObject(filters),
|
|
27
|
+
(_, __, filters) => {
|
|
28
|
+
return filters;
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
// Parse object values
|
|
32
|
+
.parse(
|
|
33
|
+
(value) => typeof value === 'object',
|
|
34
|
+
() => ({
|
|
35
|
+
transform: cloneDeep,
|
|
36
|
+
|
|
37
|
+
remove(key, data) {
|
|
38
|
+
const { [key]: ignored, ...rest } = data;
|
|
39
|
+
|
|
40
|
+
return rest;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
set(key, value, data) {
|
|
44
|
+
return { ...data, [key]: value };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
keys(data) {
|
|
48
|
+
return Object.keys(data);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
get(key, data) {
|
|
52
|
+
return data[key];
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
// Ignore null or undefined values
|
|
57
|
+
.ignore(({ value }) => isNil(value))
|
|
58
|
+
// Recursion on operators (non attributes)
|
|
59
|
+
.on(
|
|
60
|
+
({ attribute }) => isNil(attribute),
|
|
61
|
+
async ({ key, visitor, path, value, schema }, { set, recurse }) => {
|
|
62
|
+
set(key, await recurse(visitor, { schema, path }, value));
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
// Handle relation recursion
|
|
66
|
+
.onRelation(async ({ key, attribute, visitor, path, value }, { set, recurse }) => {
|
|
67
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
68
|
+
|
|
69
|
+
if (isMorphRelation) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targetSchemaUID = attribute.target;
|
|
74
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
75
|
+
|
|
76
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
77
|
+
|
|
78
|
+
set(key, newValue);
|
|
79
|
+
})
|
|
80
|
+
.onComponent(async ({ key, attribute, visitor, path, value }, { set, recurse }) => {
|
|
81
|
+
const targetSchema = strapi.getModel(attribute.component);
|
|
82
|
+
|
|
83
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
84
|
+
|
|
85
|
+
set(key, newValue);
|
|
86
|
+
})
|
|
87
|
+
// Handle media recursion
|
|
88
|
+
.onMedia(async ({ key, visitor, path, value }, { set, recurse }) => {
|
|
89
|
+
const targetSchemaUID = 'plugin::upload.file';
|
|
90
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
91
|
+
|
|
92
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
93
|
+
|
|
94
|
+
set(key, newValue);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
module.exports = curry(filters.traverse);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
curry,
|
|
5
|
+
isString,
|
|
6
|
+
isArray,
|
|
7
|
+
eq,
|
|
8
|
+
constant,
|
|
9
|
+
split,
|
|
10
|
+
isObject,
|
|
11
|
+
trim,
|
|
12
|
+
isNil,
|
|
13
|
+
cloneDeep,
|
|
14
|
+
join,
|
|
15
|
+
first,
|
|
16
|
+
} = require('lodash/fp');
|
|
17
|
+
|
|
18
|
+
const traverseFactory = require('./factory');
|
|
19
|
+
|
|
20
|
+
const isKeyword =
|
|
21
|
+
(keyword) =>
|
|
22
|
+
({ key, attribute }) => {
|
|
23
|
+
return !attribute && keyword === key;
|
|
24
|
+
};
|
|
25
|
+
const isStringArray = (value) => isArray(value) && value.every(isString);
|
|
26
|
+
|
|
27
|
+
const populate = traverseFactory()
|
|
28
|
+
// Array of strings ['foo', 'foo.bar'] => map(recurse), then filter out empty items
|
|
29
|
+
.intercept(isStringArray, async (visitor, options, populate, { recurse }) => {
|
|
30
|
+
return Promise.all(populate.map((nestedPopulate) => recurse(visitor, options, nestedPopulate)));
|
|
31
|
+
})
|
|
32
|
+
// Return wildcards as is
|
|
33
|
+
.intercept(eq('*'), constant('*'))
|
|
34
|
+
// Parse string values
|
|
35
|
+
.parse(isString, () => {
|
|
36
|
+
const tokenize = split('.');
|
|
37
|
+
const recompose = join('.');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
transform: trim,
|
|
41
|
+
|
|
42
|
+
remove(key, data) {
|
|
43
|
+
const [root] = tokenize(data);
|
|
44
|
+
|
|
45
|
+
return root === key ? undefined : data;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
set(key, value, data) {
|
|
49
|
+
const [root] = tokenize(data);
|
|
50
|
+
|
|
51
|
+
if (root !== key) {
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return isNil(value) ? root : `${root}.${value}`;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
keys(data) {
|
|
59
|
+
return [first(tokenize(data))];
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
get(key, data) {
|
|
63
|
+
const [root, ...rest] = tokenize(data);
|
|
64
|
+
|
|
65
|
+
return key === root ? recompose(rest) : undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
// Parse object values
|
|
70
|
+
.parse(isObject, () => ({
|
|
71
|
+
transform: cloneDeep,
|
|
72
|
+
|
|
73
|
+
remove(key, data) {
|
|
74
|
+
const { [key]: ignored, ...rest } = data;
|
|
75
|
+
|
|
76
|
+
return rest;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
set(key, value, data) {
|
|
80
|
+
return { ...data, [key]: value };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
keys(data) {
|
|
84
|
+
return Object.keys(data);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
get(key, data) {
|
|
88
|
+
return data[key];
|
|
89
|
+
},
|
|
90
|
+
}))
|
|
91
|
+
.ignore(({ key, attribute }) => {
|
|
92
|
+
return ['sort', 'filters', 'fields'].includes(key) && !attribute;
|
|
93
|
+
})
|
|
94
|
+
.on(
|
|
95
|
+
// Handle recursion on populate."populate"
|
|
96
|
+
isKeyword('populate'),
|
|
97
|
+
async ({ key, visitor, path, value, schema }, { set, recurse }) => {
|
|
98
|
+
const newValue = await recurse(visitor, { schema, path }, value);
|
|
99
|
+
|
|
100
|
+
set(key, newValue);
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
.on(isKeyword('on'), async ({ key, visitor, path, value }, { set, recurse }) => {
|
|
104
|
+
const newOn = {};
|
|
105
|
+
|
|
106
|
+
for (const [uid, subPopulate] of Object.entries(value)) {
|
|
107
|
+
const model = strapi.getModel(uid);
|
|
108
|
+
const newPath = { ...path, raw: `${path.raw}[${uid}]` };
|
|
109
|
+
|
|
110
|
+
const newSubPopulate = await recurse(visitor, { schema: model, path: newPath }, subPopulate);
|
|
111
|
+
|
|
112
|
+
newOn[uid] = newSubPopulate;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set(key, newOn);
|
|
116
|
+
})
|
|
117
|
+
// Handle populate on relation
|
|
118
|
+
.onRelation(async ({ key, value, attribute, visitor, path, schema }, { set, recurse }) => {
|
|
119
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
120
|
+
|
|
121
|
+
if (isMorphRelation) {
|
|
122
|
+
// Don't traverse values that cannot be parsed
|
|
123
|
+
if (!isObject(value) || !isObject(value?.on)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If there is a populate fragment defined, traverse it
|
|
128
|
+
const newValue = await recurse(visitor, { schema, path }, { on: value.on });
|
|
129
|
+
|
|
130
|
+
set(key, { on: newValue });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const targetSchemaUID = attribute.target;
|
|
134
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
135
|
+
|
|
136
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
137
|
+
|
|
138
|
+
set(key, newValue);
|
|
139
|
+
})
|
|
140
|
+
// Handle populate on media
|
|
141
|
+
.onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
|
|
142
|
+
const targetSchemaUID = 'plugin::upload.file';
|
|
143
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
144
|
+
|
|
145
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
146
|
+
|
|
147
|
+
set(key, newValue);
|
|
148
|
+
})
|
|
149
|
+
// Handle populate on components
|
|
150
|
+
.onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
|
|
151
|
+
const targetSchema = strapi.getModel(attribute.component);
|
|
152
|
+
|
|
153
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
154
|
+
|
|
155
|
+
set(key, newValue);
|
|
156
|
+
})
|
|
157
|
+
// Handle populate on dynamic zones
|
|
158
|
+
.onDynamicZone(async ({ key, value, attribute, schema, visitor, path }, { set, recurse }) => {
|
|
159
|
+
if (isObject(value)) {
|
|
160
|
+
const { components } = attribute;
|
|
161
|
+
const { on, ...properties } = value;
|
|
162
|
+
|
|
163
|
+
const newValue = {};
|
|
164
|
+
|
|
165
|
+
// Handle legacy DZ params
|
|
166
|
+
let newProperties = properties;
|
|
167
|
+
|
|
168
|
+
for (const componentUID of components) {
|
|
169
|
+
const componentSchema = strapi.getModel(componentUID);
|
|
170
|
+
newProperties = await recurse(visitor, { schema: componentSchema, path }, newProperties);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Object.assign(newValue, newProperties);
|
|
174
|
+
|
|
175
|
+
// Handle new morph fragment syntax
|
|
176
|
+
if (on) {
|
|
177
|
+
const newOn = await recurse(visitor, { schema, path }, { on });
|
|
178
|
+
|
|
179
|
+
// Recompose both syntaxes
|
|
180
|
+
Object.assign(newValue, newOn);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
set(key, newValue);
|
|
184
|
+
} else {
|
|
185
|
+
const newValue = await recurse(visitor, { schema, path }, value);
|
|
186
|
+
|
|
187
|
+
set(key, newValue);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
module.exports = curry(populate.traverse);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
curry,
|
|
5
|
+
isString,
|
|
6
|
+
isObject,
|
|
7
|
+
map,
|
|
8
|
+
trim,
|
|
9
|
+
split,
|
|
10
|
+
isEmpty,
|
|
11
|
+
flatten,
|
|
12
|
+
pipe,
|
|
13
|
+
isNil,
|
|
14
|
+
first,
|
|
15
|
+
cloneDeep,
|
|
16
|
+
} = require('lodash/fp');
|
|
17
|
+
|
|
18
|
+
const traverseFactory = require('./factory');
|
|
19
|
+
|
|
20
|
+
const ORDERS = { asc: 'asc', desc: 'desc' };
|
|
21
|
+
const ORDER_VALUES = Object.values(ORDERS);
|
|
22
|
+
|
|
23
|
+
const isSortOrder = (value) => ORDER_VALUES.includes(value.toLowerCase());
|
|
24
|
+
const isStringArray = (value) => Array.isArray(value) && value.every(isString);
|
|
25
|
+
const isObjectArray = (value) => Array.isArray(value) && value.every(isObject);
|
|
26
|
+
const isNestedSorts = (value) => isString(value) && value.split(',').length > 1;
|
|
27
|
+
|
|
28
|
+
const sort = traverseFactory()
|
|
29
|
+
.intercept(
|
|
30
|
+
// String with chained sorts (foo,bar,foobar) => split, map(recurse), then recompose
|
|
31
|
+
isNestedSorts,
|
|
32
|
+
async (visitor, options, sort, { recurse }) => {
|
|
33
|
+
return Promise.all(
|
|
34
|
+
sort
|
|
35
|
+
.split(',')
|
|
36
|
+
.map(trim)
|
|
37
|
+
.map((nestedSort) => recurse(visitor, options, nestedSort))
|
|
38
|
+
).then((res) => res.filter((part) => !isEmpty(part)).join(','));
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
.intercept(
|
|
42
|
+
// Array of strings ['foo', 'foo,bar'] => map(recurse), then filter out empty items
|
|
43
|
+
isStringArray,
|
|
44
|
+
async (visitor, options, sort, { recurse }) => {
|
|
45
|
+
return Promise.all(sort.map((nestedSort) => recurse(visitor, options, nestedSort))).then(
|
|
46
|
+
(res) => res.filter((nestedSort) => !isEmpty(nestedSort))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
.intercept(
|
|
51
|
+
// Array of objects [{ foo: 'asc' }, { bar: 'desc', baz: 'asc' }] => map(recurse), then filter out empty items
|
|
52
|
+
isObjectArray,
|
|
53
|
+
async (visitor, options, sort, { recurse }) => {
|
|
54
|
+
return Promise.all(sort.map((nestedSort) => recurse(visitor, options, nestedSort))).then(
|
|
55
|
+
(res) => res.filter((nestedSort) => !isEmpty(nestedSort))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
// Parse string values
|
|
60
|
+
.parse(
|
|
61
|
+
(sort) => typeof sort === 'string',
|
|
62
|
+
() => {
|
|
63
|
+
const tokenize = pipe(split('.'), map(split(':')), flatten);
|
|
64
|
+
const recompose = (parts) => {
|
|
65
|
+
if (parts.length === 0) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return parts.reduce((acc, part) => {
|
|
70
|
+
if (isEmpty(part)) {
|
|
71
|
+
return acc;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (acc === '') {
|
|
75
|
+
return part;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return isSortOrder(part) ? `${acc}:${part}` : `${acc}.${part}`;
|
|
79
|
+
}, '');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
transform: trim,
|
|
84
|
+
|
|
85
|
+
remove(key, data) {
|
|
86
|
+
const [root] = tokenize(data);
|
|
87
|
+
|
|
88
|
+
return root === key ? undefined : data;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
set(key, value, data) {
|
|
92
|
+
const [root] = tokenize(data);
|
|
93
|
+
|
|
94
|
+
if (root !== key) {
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return isNil(value) ? root : `${root}.${value}`;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
keys(data) {
|
|
102
|
+
return [first(tokenize(data))];
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
get(key, data) {
|
|
106
|
+
const [root, ...rest] = tokenize(data);
|
|
107
|
+
|
|
108
|
+
return key === root ? recompose(rest) : undefined;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
// Parse object values
|
|
114
|
+
.parse(
|
|
115
|
+
(value) => typeof value === 'object',
|
|
116
|
+
() => ({
|
|
117
|
+
transform: cloneDeep,
|
|
118
|
+
|
|
119
|
+
remove(key, data) {
|
|
120
|
+
const { [key]: ignored, ...rest } = data;
|
|
121
|
+
|
|
122
|
+
return rest;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
set(key, value, data) {
|
|
126
|
+
return { ...data, [key]: value };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
keys(data) {
|
|
130
|
+
return Object.keys(data);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
get(key, data) {
|
|
134
|
+
return data[key];
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
)
|
|
138
|
+
// Handle deep sort on relation
|
|
139
|
+
.onRelation(async ({ key, value, attribute, visitor, path }, { set, recurse }) => {
|
|
140
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
141
|
+
|
|
142
|
+
if (isMorphRelation) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const targetSchemaUID = attribute.target;
|
|
147
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
148
|
+
|
|
149
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
150
|
+
|
|
151
|
+
set(key, newValue);
|
|
152
|
+
})
|
|
153
|
+
// Handle deep sort on media
|
|
154
|
+
.onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
|
|
155
|
+
const targetSchemaUID = 'plugin::upload.file';
|
|
156
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
157
|
+
|
|
158
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
159
|
+
|
|
160
|
+
set(key, newValue);
|
|
161
|
+
})
|
|
162
|
+
// Handle deep sort on components
|
|
163
|
+
.onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
|
|
164
|
+
const targetSchema = strapi.getModel(attribute.component);
|
|
165
|
+
|
|
166
|
+
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
|
|
167
|
+
|
|
168
|
+
set(key, newValue);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
module.exports = curry(sort.traverse);
|
package/lib/traverse-entity.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { cloneDeep, isObject, isArray, isNil, curry } = require('lodash/fp');
|
|
4
4
|
|
|
5
5
|
const traverseEntity = async (visitor, options, entity) => {
|
|
6
|
-
const { path = null, schema } = options;
|
|
6
|
+
const { path = { raw: null, attribute: null }, schema } = options;
|
|
7
7
|
|
|
8
8
|
// End recursion
|
|
9
9
|
if (!isObject(entity) || isNil(schema)) {
|
|
@@ -22,7 +22,13 @@ const traverseEntity = async (visitor, options, entity) => {
|
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const newPath = path
|
|
25
|
+
const newPath = { ...path };
|
|
26
|
+
|
|
27
|
+
newPath.raw = isNil(path.raw) ? key : `${path.raw}.${key}`;
|
|
28
|
+
|
|
29
|
+
if (!isNil(attribute)) {
|
|
30
|
+
newPath.attribute = isNil(path.attribute) ? key : `${path.attribute}.${key}`;
|
|
31
|
+
}
|
|
26
32
|
|
|
27
33
|
// Visit the current attribute
|
|
28
34
|
const visitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/utils",
|
|
3
|
-
"version": "4.9.0-
|
|
3
|
+
"version": "4.9.0-beta.1",
|
|
4
4
|
"description": "Shared utilities for the Strapi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"node": ">=14.19.1 <=18.x.x",
|
|
47
47
|
"npm": ">=6.0.0"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "ff37d666d0634fc84827e3d6419d916618275572"
|
|
50
50
|
}
|