@strapi/utils 4.11.0-exp.push-transfer-push-stuck → 4.11.1-beta.0

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.
@@ -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,24 @@ 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) =>
96
+ union(
97
+ strapi?.config?.get('api.responses.privateAttributes', []) ?? [],
98
+ getOr([], 'options.privateAttributes', model)
99
+ );
100
+
95
101
  const getPrivateAttributes = (model = {}) => {
96
102
  return _.union(
97
- strapi.config.get('api.responses.privateAttributes', []),
98
- _.get(model, 'options.privateAttributes', []),
103
+ getStoredPrivateAttributes(model),
99
104
  _.keys(_.pickBy(model.attributes, (attr) => !!attr.private))
100
105
  );
101
106
  };
102
107
 
103
108
  const isPrivateAttribute = (model, attributeName) => {
104
- return model?.privateAttributes?.includes(attributeName) ?? false;
109
+ if (model?.attributes?.[attributeName]?.private === true) {
110
+ return true;
111
+ }
112
+ return getStoredPrivateAttributes(model).includes(attributeName);
105
113
  };
106
114
 
107
115
  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,8 +28,8 @@ 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');
32
+ const webhook = require('./webhook');
33
33
  const relations = require('./relations');
34
34
  const setCreatorFields = require('./set-creator-fields');
35
35
  const hooks = require('./hooks');
@@ -43,6 +43,7 @@ const importDefault = require('./import-default');
43
43
  const template = require('./template');
44
44
  const file = require('./file');
45
45
  const traverse = require('./traverse');
46
+ const { isOperator, isOperatorOfType } = require('./operators');
46
47
 
47
48
  module.exports = {
48
49
  yup,
@@ -74,8 +75,8 @@ module.exports = {
74
75
  isKebabCase,
75
76
  isCamelCase,
76
77
  toKebabCase,
77
- contentTypes,
78
78
  webhook,
79
+ contentTypes,
79
80
  env,
80
81
  relations,
81
82
  setCreatorFields,
@@ -93,4 +94,6 @@ module.exports = {
93
94
  importDefault,
94
95
  file,
95
96
  traverse,
97
+ isOperator,
98
+ isOperatorOfType,
96
99
  };
@@ -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) || attribute.private === true;
10
+ const isPrivate = attribute.private === true || isPrivateAttribute(schema, key);
11
11
 
12
12
  if (isPrivate) {
13
13
  remove(key);
package/lib/webhook.js CHANGED
@@ -11,6 +11,20 @@ const webhookEvents = {
11
11
  MEDIA_DELETE: 'media.delete',
12
12
  };
13
13
 
14
+ /**
15
+ * TODO V5: remove this file
16
+ * @deprecated
17
+ */
18
+ const deprecatedWebhookEvents = new Proxy(webhookEvents, {
19
+ get(target, prop) {
20
+ console.warn(
21
+ '[deprecated] @strapi/utils/webhook will no longer exist in the next major release of Strapi. ' +
22
+ 'Instead, the webhookEvents object can be retrieved from strapi.webhookStore.allowedEvents'
23
+ );
24
+ return target[prop];
25
+ },
26
+ });
27
+
14
28
  module.exports = {
15
- webhookEvents,
29
+ webhookEvents: deprecatedWebhookEvents,
16
30
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/utils",
3
- "version": "4.11.0-exp.push-transfer-push-stuck",
3
+ "version": "4.11.1-beta.0",
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": "90032a791f5413b9f139ea01687eda04c057eacd"
52
+ "gitHead": "30e56b8376b9cb52c39ecd1c3b7d8706688a1685"
53
53
  }