@strapi/utils 4.7.1 → 4.7.2-exp.24dd7d95972fa822bf43e9b095b51027402c229e

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.
@@ -232,7 +232,8 @@ const convertPopulateObject = (populate, schema) => {
232
232
  isMediaAttribute(attribute) ||
233
233
  isMorphToRelationalAttribute(attribute);
234
234
 
235
- const hasFragmentPopulateDefined = typeof subPopulate === 'object' && 'on' in subPopulate;
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.attributes);
422
+ const attribute = get(key, schema?.attributes);
422
423
 
423
424
  // Handle attributes
424
425
  if (attribute) {
package/lib/index.js CHANGED
@@ -41,6 +41,7 @@ const { pipeAsync, mapAsync, reduceAsync, forEachAsync } = require('./async');
41
41
  const convertQueryParams = require('./convert-query-params');
42
42
  const importDefault = require('./import-default');
43
43
  const template = require('./template');
44
+ const traverse = require('./traverse');
44
45
  const file = require('./file');
45
46
 
46
47
  module.exports = {
@@ -90,5 +91,6 @@ module.exports = {
90
91
  validateYupSchemaSync,
91
92
  convertQueryParams,
92
93
  importDefault,
94
+ traverse,
93
95
  file,
94
96
  };
@@ -1,13 +1,15 @@
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');
11
13
 
12
14
  module.exports = {
13
15
  contentAPI: {
@@ -54,6 +56,92 @@ module.exports = {
54
56
 
55
57
  return pipeAsync(...transforms)(data);
56
58
  },
59
+
60
+ async params(params, schema, { auth } = {}) {
61
+ const { filters, sort, fields, populate } = params;
62
+
63
+ const sanitizedParams = cloneDeep(params);
64
+
65
+ if (filters) {
66
+ Object.assign(sanitizedParams, { filters: await this.filters(filters, schema, { auth }) });
67
+ }
68
+
69
+ if (sort) {
70
+ Object.assign(sanitizedParams, { sort: await this.sort(sort, schema, { auth }) });
71
+ }
72
+
73
+ if (fields) {
74
+ Object.assign(sanitizedParams, { fields: await this.fields(fields, schema) });
75
+ }
76
+
77
+ if (populate) {
78
+ Object.assign(sanitizedParams, { populate: await this.populate(populate, schema) });
79
+ }
80
+
81
+ return sanitizedParams;
82
+ },
83
+
84
+ filters(filters, schema, { auth } = {}) {
85
+ if (isArray(filters)) {
86
+ return Promise.all(filters.map((filter) => this.filters(filter, schema, { auth })));
87
+ }
88
+
89
+ const transforms = [sanitizers.defaultSanitizeFilters(schema)];
90
+
91
+ if (auth) {
92
+ transforms.push(traverseQueryFilters(visitors.removeRestrictedRelations(auth), { schema }));
93
+ }
94
+
95
+ // Apply sanitizers from registry if exists
96
+ strapi.sanitizers
97
+ .get('content-api.filters')
98
+ .forEach((sanitizer) => transforms.push(sanitizer(schema)));
99
+
100
+ return pipeAsync(...transforms)(filters);
101
+ },
102
+
103
+ sort(sort, schema, { auth } = {}) {
104
+ const transforms = [sanitizers.defaultSanitizeSort(schema)];
105
+
106
+ if (auth) {
107
+ transforms.push(traverseQuerySort(visitors.removeRestrictedRelations(auth), { schema }));
108
+ }
109
+
110
+ // Apply sanitizers from registry if exists
111
+ strapi.sanitizers
112
+ .get('content-api.sort')
113
+ .forEach((sanitizer) => transforms.push(sanitizer(schema)));
114
+
115
+ return pipeAsync(...transforms)(sort);
116
+ },
117
+
118
+ fields(fields, schema) {
119
+ const transforms = [sanitizers.defaultSanitizeFields(schema)];
120
+
121
+ // Apply sanitizers from registry if exists
122
+ strapi.sanitizers
123
+ .get('content-api.fields')
124
+ .forEach((sanitizer) => transforms.push(sanitizer(schema)));
125
+
126
+ return pipeAsync(...transforms)(fields);
127
+ },
128
+
129
+ populate(populate, schema, { auth } = {}) {
130
+ const transforms = [sanitizers.defaultSanitizePopulate(schema)];
131
+
132
+ if (auth) {
133
+ transforms.push(
134
+ traverseQueryPopulate(visitors.removeRestrictedRelations(auth), { schema })
135
+ );
136
+ }
137
+
138
+ // Apply sanitizers from registry if exists
139
+ strapi.sanitizers
140
+ .get('content-api.populate')
141
+ .forEach((sanitizer) => transforms.push(sanitizer(schema)));
142
+
143
+ return pipeAsync(...transforms)(populate);
144
+ },
57
145
  },
58
146
 
59
147
  sanitizers,
@@ -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 { removePassword, removePrivate } = require('./visitors');
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,106 @@ 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 private from filters
42
+ traverseQueryFilters(removePrivate, { schema }),
43
+ // Remove empty objects
44
+ traverseQueryFilters(
45
+ ({ key, value }, { remove }) => {
46
+ if (isObject(value) && isEmpty(value)) {
47
+ remove(key);
48
+ }
49
+ },
50
+ { schema }
51
+ )
52
+ )(filters);
53
+ });
54
+
55
+ const defaultSanitizeSort = curry((schema, sort) => {
56
+ return pipeAsync(
57
+ // Remove non attribute keys
58
+ traverseQuerySort(
59
+ ({ key, attribute }, { remove }) => {
60
+ if (!attribute) {
61
+ remove(key);
62
+ }
63
+ },
64
+ { schema }
65
+ ),
66
+ // Remove dynamic zones from sort
67
+ traverseQuerySort(removeDynamicZones, { schema }),
68
+ // Remove morpTo relations from sort
69
+ traverseQuerySort(removeMorphToRelations, { schema }),
70
+ // Remove private from sort
71
+ traverseQuerySort(removePrivate, { schema }),
72
+ // Remove keys for empty non-scalar values
73
+ traverseQuerySort(
74
+ ({ key, attribute, value }, { remove }) => {
75
+ if (!isScalarAttribute(attribute) && isEmpty(value)) {
76
+ remove(key);
77
+ }
78
+ },
79
+ { schema }
80
+ )
81
+ )(sort);
82
+ });
83
+
84
+ const defaultSanitizeFields = curry((schema, fields) => {
85
+ return pipeAsync(
86
+ // Only keep scalar attributes
87
+ traverseQueryFields(
88
+ ({ key, attribute }, { remove }) => {
89
+ if (isNil(attribute) || !isScalarAttribute(attribute)) {
90
+ remove(key);
91
+ }
92
+ },
93
+ { schema }
94
+ ),
95
+ // Remove private fields
96
+ traverseQueryFields(removePrivate, { schema }),
97
+ // Remove nil values from fields array
98
+ (value) => (isArray(value) ? value.filter((field) => !isNil(field)) : value)
99
+ )(fields);
100
+ });
101
+
102
+ const defaultSanitizePopulate = curry((schema, populate) => {
103
+ return pipeAsync(
104
+ traverseQueryPopulate(
105
+ async ({ key, value, schema, attribute }, { set }) => {
106
+ if (attribute) {
107
+ return;
108
+ }
109
+
110
+ if (key === 'sort') {
111
+ set(key, await defaultSanitizeSort(schema, value));
112
+ }
113
+
114
+ if (key === 'filters') {
115
+ set(key, await defaultSanitizeFilters(schema, value));
116
+ }
117
+
118
+ if (key === 'fields') {
119
+ set(key, await defaultSanitizeFields(schema, value));
120
+ }
121
+ },
122
+ { schema }
123
+ ),
124
+ // Remove private fields
125
+ traverseQueryPopulate(removePrivate, { schema })
126
+ )(populate);
127
+ });
128
+
22
129
  module.exports = {
23
130
  sanitizePasswords,
24
131
  sanitizePrivates,
25
132
  defaultSanitizeOutput,
133
+ defaultSanitizeFilters,
134
+ defaultSanitizeSort,
135
+ defaultSanitizeFields,
136
+ defaultSanitizePopulate,
26
137
  };
@@ -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
  };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const { isDynamicZoneAttribute } = require('../../content-types');
4
+
5
+ module.exports = ({ key, attribute }, { remove }) => {
6
+ if (isDynamicZoneAttribute(attribute)) {
7
+ remove(key);
8
+ }
9
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const { isMorphToRelationalAttribute } = require('../../content-types');
4
+
5
+ module.exports = ({ key, attribute }, { remove }) => {
6
+ if (isMorphToRelationalAttribute(attribute)) {
7
+ remove(key);
8
+ }
9
+ };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  module.exports = ({ key, attribute }, { remove }) => {
4
- if (attribute.type === 'password') {
4
+ if (attribute?.type === 'password') {
5
5
  remove(key);
6
6
  }
7
7
  };
@@ -2,8 +2,12 @@
2
2
 
3
3
  const { isPrivateAttribute } = require('../../content-types');
4
4
 
5
- module.exports = ({ schema, key }, { remove }) => {
6
- const isPrivate = isPrivateAttribute(schema, key);
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) {
@@ -4,7 +4,7 @@ const { isArray } = require('lodash/fp');
4
4
 
5
5
  module.exports =
6
6
  (restrictedFields = null) =>
7
- ({ key, path }, { remove }) => {
7
+ ({ key, path: { attribute: path } }, { remove }) => {
8
8
  // Remove all fields
9
9
  if (restrictedFields === null) {
10
10
  remove(key);
@@ -0,0 +1,153 @@
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 = schema?.attributes?.[key];
41
+
42
+ const newPath = { ...path };
43
+
44
+ newPath.raw = isNil(path.raw) ? key : `${path.raw}.${key}`;
45
+
46
+ if (!isNil(attribute)) {
47
+ newPath.attribute = isNil(path.attribute) ? key : `${path.attribute}.${key}`;
48
+ }
49
+
50
+ // visitors
51
+
52
+ const visitorOptions = {
53
+ key,
54
+ value: utils.get(key, out),
55
+ attribute,
56
+ schema,
57
+ path: newPath,
58
+ data: out,
59
+ };
60
+
61
+ const transformUtils = {
62
+ remove(key) {
63
+ out = utils.remove(key, out);
64
+ },
65
+ set(key, value) {
66
+ out = utils.set(key, value, out);
67
+ },
68
+ recurse: traverse,
69
+ };
70
+
71
+ await visitor(visitorOptions, pick(['remove', 'set'], transformUtils));
72
+
73
+ const value = utils.get(key, out);
74
+
75
+ const createContext = () => ({
76
+ key,
77
+ value,
78
+ attribute,
79
+ schema,
80
+ path: newPath,
81
+ data: out,
82
+ visitor,
83
+ });
84
+
85
+ // ignore
86
+ const ignoreCtx = createContext();
87
+ const shouldIgnore = state.ignore.some((predicate) => predicate(ignoreCtx));
88
+
89
+ if (shouldIgnore) {
90
+ continue;
91
+ }
92
+
93
+ // handlers
94
+ const handlers = [...state.handlers.common, ...state.handlers.attributes];
95
+
96
+ for await (const handler of handlers) {
97
+ const ctx = createContext();
98
+ const pass = await handler.predicate(ctx);
99
+
100
+ if (pass) {
101
+ await handler.handler(ctx, pick(['recurse', 'set'], transformUtils));
102
+ }
103
+ }
104
+ }
105
+
106
+ return out;
107
+ };
108
+
109
+ return {
110
+ traverse,
111
+
112
+ intercept(predicate, handler) {
113
+ state.interceptors.push({ predicate, handler });
114
+ return this;
115
+ },
116
+
117
+ parse(predicate, parser) {
118
+ state.parsers.push({ predicate, parser });
119
+ return this;
120
+ },
121
+
122
+ ignore(predicate) {
123
+ state.ignore.push(predicate);
124
+ return this;
125
+ },
126
+
127
+ on(predicate, handler) {
128
+ state.handlers.common.push({ predicate, handler });
129
+ return this;
130
+ },
131
+
132
+ onAttribute(predicate, handler) {
133
+ state.handlers.attributes.push({ predicate, handler });
134
+ return this;
135
+ },
136
+
137
+ onRelation(handler) {
138
+ return this.onAttribute(({ attribute }) => attribute?.type === 'relation', handler);
139
+ },
140
+
141
+ onMedia(handler) {
142
+ return this.onAttribute(({ attribute }) => attribute?.type === 'media', handler);
143
+ },
144
+
145
+ onComponent(handler) {
146
+ return this.onAttribute(({ attribute }) => attribute?.type === 'component', handler);
147
+ },
148
+
149
+ onDynamicZone(handler) {
150
+ return this.onAttribute(({ attribute }) => attribute?.type === 'dynamiczone', handler);
151
+ },
152
+ };
153
+ };
@@ -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 or $or can have a value
15
+ // 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,185 @@
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
+ // Handle legacy DZ params
164
+ let newProperties = properties;
165
+
166
+ for (const componentUID of components) {
167
+ const componentSchema = strapi.getModel(componentUID);
168
+ newProperties = await recurse(visitor, { schema: componentSchema, path }, newProperties);
169
+ }
170
+
171
+ // Handle new morph fragment syntax
172
+ const newOn = await recurse(visitor, { schema, path }, { on });
173
+
174
+ // Recompose both syntaxes
175
+ const newValue = { ...newOn, ...newProperties };
176
+
177
+ set(key, newValue);
178
+ } else {
179
+ const newValue = await recurse(visitor, { schema, path }, value);
180
+
181
+ set(key, newValue);
182
+ }
183
+ });
184
+
185
+ module.exports = curry(populate.traverse);
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ curry,
5
+ isString,
6
+ map,
7
+ trim,
8
+ split,
9
+ isEmpty,
10
+ flatten,
11
+ pipe,
12
+ isNil,
13
+ first,
14
+ cloneDeep,
15
+ } = require('lodash/fp');
16
+
17
+ const traverseFactory = require('./factory');
18
+
19
+ const ORDERS = { asc: 'asc', desc: 'desc' };
20
+ const ORDER_VALUES = Object.values(ORDERS);
21
+
22
+ const isSortOrder = (value) => ORDER_VALUES.includes(value.toLowerCase());
23
+ const isStringArray = (value) => Array.isArray(value) && value.every(isString);
24
+ const isNestedSorts = (value) => isString(value) && value.split(',').length > 1;
25
+
26
+ const sort = traverseFactory()
27
+ .intercept(
28
+ // String with chained sorts (foo,bar,foobar) => split, map(recurse), then recompose
29
+ isNestedSorts,
30
+ async (visitor, options, sort, { recurse }) => {
31
+ return Promise.all(
32
+ sort
33
+ .split(',')
34
+ .map(trim)
35
+ .map((nestedSort) => recurse(visitor, options, nestedSort))
36
+ ).then((res) => res.filter((part) => !isEmpty(part)).join(','));
37
+ }
38
+ )
39
+ .intercept(
40
+ // Array of strings ['foo', 'foo,bar'] => map(recurse), then filter out empty items
41
+ isStringArray,
42
+ async (visitor, options, sort, { recurse }) => {
43
+ return Promise.all(sort.map((nestedSort) => recurse(visitor, options, nestedSort))).then(
44
+ (res) => res.filter((nestedSort) => !isEmpty(nestedSort))
45
+ );
46
+ }
47
+ )
48
+ // Parse string values
49
+ .parse(
50
+ (sort) => typeof sort === 'string',
51
+ () => {
52
+ const tokenize = pipe(split('.'), map(split(':')), flatten);
53
+ const recompose = (parts) => {
54
+ if (parts.length === 0) {
55
+ return undefined;
56
+ }
57
+
58
+ return parts.reduce((acc, part) => {
59
+ if (isEmpty(part)) {
60
+ return acc;
61
+ }
62
+
63
+ if (acc === '') {
64
+ return part;
65
+ }
66
+
67
+ return isSortOrder(part) ? `${acc}:${part}` : `${acc}.${part}`;
68
+ }, '');
69
+ };
70
+
71
+ return {
72
+ transform: trim,
73
+
74
+ remove(key, data) {
75
+ const [root] = tokenize(data);
76
+
77
+ return root === key ? undefined : data;
78
+ },
79
+
80
+ set(key, value, data) {
81
+ const [root] = tokenize(data);
82
+
83
+ if (root !== key) {
84
+ return data;
85
+ }
86
+
87
+ return isNil(value) ? root : `${root}.${value}`;
88
+ },
89
+
90
+ keys(data) {
91
+ return [first(tokenize(data))];
92
+ },
93
+
94
+ get(key, data) {
95
+ const [root, ...rest] = tokenize(data);
96
+
97
+ return key === root ? recompose(rest) : undefined;
98
+ },
99
+ };
100
+ }
101
+ )
102
+ // Parse object values
103
+ .parse(
104
+ (value) => typeof value === 'object',
105
+ () => ({
106
+ transform: cloneDeep,
107
+
108
+ remove(key, data) {
109
+ const { [key]: ignored, ...rest } = data;
110
+
111
+ return rest;
112
+ },
113
+
114
+ set(key, value, data) {
115
+ return { ...data, [key]: value };
116
+ },
117
+
118
+ keys(data) {
119
+ return Object.keys(data);
120
+ },
121
+
122
+ get(key, data) {
123
+ return data[key];
124
+ },
125
+ })
126
+ )
127
+ // Handle deep sort on relation
128
+ .onRelation(async ({ key, value, attribute, visitor, path }, { set, recurse }) => {
129
+ const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
130
+
131
+ if (isMorphRelation) {
132
+ return;
133
+ }
134
+
135
+ const targetSchemaUID = attribute.target;
136
+ const targetSchema = strapi.getModel(targetSchemaUID);
137
+
138
+ const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
139
+
140
+ set(key, newValue);
141
+ })
142
+ // Handle deep sort on media
143
+ .onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
144
+ const targetSchemaUID = 'plugin::upload.file';
145
+ const targetSchema = strapi.getModel(targetSchemaUID);
146
+
147
+ const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
148
+
149
+ set(key, newValue);
150
+ })
151
+ // Handle deep sort on components
152
+ .onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
153
+ const targetSchema = strapi.getModel(attribute.component);
154
+
155
+ const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
156
+
157
+ set(key, newValue);
158
+ });
159
+
160
+ module.exports = curry(sort.traverse);
@@ -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 ? `${path}.${key}` : key;
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.7.1",
3
+ "version": "4.7.2-exp.24dd7d95972fa822bf43e9b095b51027402c229e",
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": "0307fb4bf7b006c4cd902412967d3987d7810ed8"
49
+ "gitHead": "24dd7d95972fa822bf43e9b095b51027402c229e"
50
50
  }