@strapi/utils 0.0.0-fd8e4c6bfa → 0.0.0-ffd0a215143bcafd0354d791533a57a084e53ffb
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/async.d.ts +12 -0
- package/lib/async.js +34 -0
- package/lib/build-query.js +1 -1
- package/lib/content-types.js +23 -5
- package/lib/convert-query-params.js +177 -9
- package/lib/errors.js +17 -7
- package/lib/file.js +60 -0
- package/lib/hooks.js +19 -12
- package/lib/import-default.js +10 -0
- package/lib/index.js +13 -2
- package/lib/object-formatting.js +6 -0
- package/lib/print-value.js +1 -0
- package/lib/provider-factory.js +1 -17
- package/lib/relations.js +15 -0
- package/lib/sanitize/index.js +1 -1
- package/lib/sanitize/sanitizers.js +1 -1
- package/lib/template.js +28 -0
- package/package.json +5 -4
- package/lib/pipe-async.js +0 -13
package/lib/async.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MapAsync<T = any, R = any> = lodash.CurriedFunction3<
|
|
2
|
+
T[],
|
|
3
|
+
(element: T, index: number) => R | Promise<R>,
|
|
4
|
+
{ concurrency?: number },
|
|
5
|
+
Promise<R[]>
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export type ForEachAsync<T = any, R = any> = (
|
|
9
|
+
array: T[],
|
|
10
|
+
func: (element: T, index: number) => R | Promise<R>,
|
|
11
|
+
options?: { concurrency?: number }
|
|
12
|
+
) => Promise<R[]>;
|
package/lib/async.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pMap = require('p-map');
|
|
4
|
+
const { curry } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
function pipeAsync(...methods) {
|
|
7
|
+
return async (data) => {
|
|
8
|
+
let res = data;
|
|
9
|
+
|
|
10
|
+
for (const method of methods) {
|
|
11
|
+
res = await method(res);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return res;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @type { import('./async').MapAsync }
|
|
20
|
+
*/
|
|
21
|
+
const mapAsync = curry(pMap);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @type { import('./async').ForEachAsync }
|
|
25
|
+
*/
|
|
26
|
+
const forEachAsync = curry(async (array, func, options) => {
|
|
27
|
+
await mapAsync(array, func, options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
mapAsync,
|
|
32
|
+
forEachAsync,
|
|
33
|
+
pipeAsync,
|
|
34
|
+
};
|
package/lib/build-query.js
CHANGED
package/lib/content-types.js
CHANGED
|
@@ -104,7 +104,26 @@ const isPrivateAttribute = (model = {}, attributeName) => {
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
const isScalarAttribute = (attribute) => {
|
|
107
|
-
return !['media', 'component', 'relation', 'dynamiczone'].includes(attribute
|
|
107
|
+
return !['media', 'component', 'relation', 'dynamiczone'].includes(attribute?.type);
|
|
108
|
+
};
|
|
109
|
+
const isMediaAttribute = (attribute) => attribute?.type === 'media';
|
|
110
|
+
const isRelationalAttribute = (attribute) => attribute?.type === 'relation';
|
|
111
|
+
const isComponentAttribute = (attribute) => ['component', 'dynamiczone'].includes(attribute?.type);
|
|
112
|
+
|
|
113
|
+
const isDynamicZoneAttribute = (attribute) => attribute?.type === 'dynamiczone';
|
|
114
|
+
const isMorphToRelationalAttribute = (attribute) => {
|
|
115
|
+
return isRelationalAttribute(attribute) && attribute?.relation?.startsWith?.('morphTo');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getComponentAttributes = (schema) => {
|
|
119
|
+
return _.reduce(
|
|
120
|
+
schema.attributes,
|
|
121
|
+
(acc, attr, attrName) => {
|
|
122
|
+
if (isComponentAttribute(attr)) acc.push(attrName);
|
|
123
|
+
return acc;
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
108
127
|
};
|
|
109
128
|
|
|
110
129
|
const getScalarAttributes = (schema) => {
|
|
@@ -118,10 +137,6 @@ const getScalarAttributes = (schema) => {
|
|
|
118
137
|
);
|
|
119
138
|
};
|
|
120
139
|
|
|
121
|
-
const isMediaAttribute = (attribute) => attribute.type === 'media';
|
|
122
|
-
const isRelationalAttribute = (attribute) => attribute.type === 'relation';
|
|
123
|
-
const isComponentAttribute = (attribute) => ['component', 'dynamiczone'].includes(attribute.type);
|
|
124
|
-
|
|
125
140
|
/**
|
|
126
141
|
* Checks if an attribute is of type `type`
|
|
127
142
|
* @param {object} attribute
|
|
@@ -147,11 +162,14 @@ module.exports = {
|
|
|
147
162
|
isMediaAttribute,
|
|
148
163
|
isRelationalAttribute,
|
|
149
164
|
isComponentAttribute,
|
|
165
|
+
isDynamicZoneAttribute,
|
|
166
|
+
isMorphToRelationalAttribute,
|
|
150
167
|
isTypedAttribute,
|
|
151
168
|
getPrivateAttributes,
|
|
152
169
|
isPrivateAttribute,
|
|
153
170
|
constants,
|
|
154
171
|
getNonWritableAttributes,
|
|
172
|
+
getComponentAttributes,
|
|
155
173
|
getScalarAttributes,
|
|
156
174
|
getWritableAttributes,
|
|
157
175
|
isWritableAttribute,
|
|
@@ -6,10 +6,28 @@
|
|
|
6
6
|
* Converts the standard Strapi REST query params to a more usable format for querying
|
|
7
7
|
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
isNil,
|
|
12
|
+
toNumber,
|
|
13
|
+
isInteger,
|
|
14
|
+
has,
|
|
15
|
+
isEmpty,
|
|
16
|
+
isObject,
|
|
17
|
+
isPlainObject,
|
|
18
|
+
cloneDeep,
|
|
19
|
+
get,
|
|
20
|
+
mergeAll,
|
|
21
|
+
} = require('lodash/fp');
|
|
10
22
|
const _ = require('lodash');
|
|
11
23
|
const parseType = require('./parse-type');
|
|
12
24
|
const contentTypesUtils = require('./content-types');
|
|
25
|
+
const { PaginationError } = require('./errors');
|
|
26
|
+
const {
|
|
27
|
+
isMediaAttribute,
|
|
28
|
+
isDynamicZoneAttribute,
|
|
29
|
+
isMorphToRelationalAttribute,
|
|
30
|
+
} = require('./content-types');
|
|
13
31
|
|
|
14
32
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
|
15
33
|
|
|
@@ -37,6 +55,10 @@ const convertCountQueryParams = (countQuery) => {
|
|
|
37
55
|
return parseType({ type: 'boolean', value: countQuery });
|
|
38
56
|
};
|
|
39
57
|
|
|
58
|
+
const convertOrderingQueryParams = (ordering) => {
|
|
59
|
+
return ordering;
|
|
60
|
+
};
|
|
61
|
+
|
|
40
62
|
/**
|
|
41
63
|
* Sort query parser
|
|
42
64
|
* @param {string} sortQuery - ex: id:asc,price:desc
|
|
@@ -117,6 +139,41 @@ const convertLimitQueryParams = (limitQuery) => {
|
|
|
117
139
|
return limitAsANumber;
|
|
118
140
|
};
|
|
119
141
|
|
|
142
|
+
const convertPageQueryParams = (page) => {
|
|
143
|
+
const pageVal = toNumber(page);
|
|
144
|
+
|
|
145
|
+
if (!isInteger(pageVal) || pageVal <= 0) {
|
|
146
|
+
throw new PaginationError(
|
|
147
|
+
`Invalid 'page' parameter. Expected an integer > 0, received: ${page}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return pageVal;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const convertPageSizeQueryParams = (pageSize, page) => {
|
|
155
|
+
const pageSizeVal = toNumber(pageSize);
|
|
156
|
+
|
|
157
|
+
if (!isInteger(pageSizeVal) || pageSizeVal <= 0) {
|
|
158
|
+
throw new PaginationError(
|
|
159
|
+
`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return pageSizeVal;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const validatePaginationParams = (page, pageSize, start, limit) => {
|
|
167
|
+
const isPagePagination = !isNil(page) || !isNil(pageSize);
|
|
168
|
+
const isOffsetPagination = !isNil(start) || !isNil(limit);
|
|
169
|
+
|
|
170
|
+
if (isPagePagination && isOffsetPagination) {
|
|
171
|
+
throw new PaginationError(
|
|
172
|
+
'Invalid pagination attributes. You cannot use page and offset pagination in the same query'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
120
177
|
class InvalidPopulateError extends Error {
|
|
121
178
|
constructor() {
|
|
122
179
|
super();
|
|
@@ -169,12 +226,41 @@ const convertPopulateObject = (populate, schema) => {
|
|
|
169
226
|
return acc;
|
|
170
227
|
}
|
|
171
228
|
|
|
172
|
-
//
|
|
173
|
-
|
|
229
|
+
// Allow adding an 'on' strategy to populate queries for polymorphic relations, media and dynamic zones
|
|
230
|
+
const isAllowedAttributeForFragmentPopulate =
|
|
231
|
+
isDynamicZoneAttribute(attribute) ||
|
|
232
|
+
isMediaAttribute(attribute) ||
|
|
233
|
+
isMorphToRelationalAttribute(attribute);
|
|
234
|
+
|
|
235
|
+
const hasFragmentPopulateDefined = typeof subPopulate === 'object' && 'on' in subPopulate;
|
|
236
|
+
|
|
237
|
+
if (isAllowedAttributeForFragmentPopulate && hasFragmentPopulateDefined) {
|
|
238
|
+
return {
|
|
239
|
+
...acc,
|
|
240
|
+
[key]: {
|
|
241
|
+
on: Object.entries(subPopulate.on).reduce(
|
|
242
|
+
(acc, [type, typeSubPopulate]) => ({
|
|
243
|
+
...acc,
|
|
244
|
+
[type]: convertNestedPopulate(typeSubPopulate, strapi.getModel(type)),
|
|
245
|
+
}),
|
|
246
|
+
{}
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// TODO: This is a query's populate fallback for DynamicZone and is kept for legacy purpose.
|
|
253
|
+
// Removing it could break existing user queries but it should be removed in V5.
|
|
174
254
|
if (attribute.type === 'dynamiczone') {
|
|
175
255
|
const populates = attribute.components
|
|
176
256
|
.map((uid) => strapi.getModel(uid))
|
|
177
|
-
.map((schema) => convertNestedPopulate(subPopulate, schema))
|
|
257
|
+
.map((schema) => convertNestedPopulate(subPopulate, schema))
|
|
258
|
+
.map((populate) => (populate === true ? {} : populate)) // cast boolean to empty object to avoid merging issues
|
|
259
|
+
.filter((populate) => populate !== false);
|
|
260
|
+
|
|
261
|
+
if (isEmpty(populates)) {
|
|
262
|
+
return acc;
|
|
263
|
+
}
|
|
178
264
|
|
|
179
265
|
return {
|
|
180
266
|
...acc,
|
|
@@ -203,16 +289,22 @@ const convertPopulateObject = (populate, schema) => {
|
|
|
203
289
|
return acc;
|
|
204
290
|
}
|
|
205
291
|
|
|
292
|
+
const populateObject = convertNestedPopulate(subPopulate, targetSchema);
|
|
293
|
+
|
|
294
|
+
if (!populateObject) {
|
|
295
|
+
return acc;
|
|
296
|
+
}
|
|
297
|
+
|
|
206
298
|
return {
|
|
207
299
|
...acc,
|
|
208
|
-
[key]:
|
|
300
|
+
[key]: populateObject,
|
|
209
301
|
};
|
|
210
302
|
}, {});
|
|
211
303
|
};
|
|
212
304
|
|
|
213
305
|
const convertNestedPopulate = (subPopulate, schema) => {
|
|
214
|
-
if (subPopulate
|
|
215
|
-
return true;
|
|
306
|
+
if (_.isString(subPopulate)) {
|
|
307
|
+
return parseType({ type: 'boolean', value: subPopulate, forceCast: true });
|
|
216
308
|
}
|
|
217
309
|
|
|
218
310
|
if (_.isBoolean(subPopulate)) {
|
|
@@ -223,8 +315,8 @@ const convertNestedPopulate = (subPopulate, schema) => {
|
|
|
223
315
|
throw new Error(`Invalid nested populate. Expected '*' or an object`);
|
|
224
316
|
}
|
|
225
317
|
|
|
226
|
-
|
|
227
|
-
|
|
318
|
+
const { sort, filters, fields, populate, count, ordering, page, pageSize, start, limit } =
|
|
319
|
+
subPopulate;
|
|
228
320
|
|
|
229
321
|
const query = {};
|
|
230
322
|
|
|
@@ -248,6 +340,30 @@ const convertNestedPopulate = (subPopulate, schema) => {
|
|
|
248
340
|
query.count = convertCountQueryParams(count);
|
|
249
341
|
}
|
|
250
342
|
|
|
343
|
+
if (ordering) {
|
|
344
|
+
query.ordering = convertOrderingQueryParams(ordering);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
validatePaginationParams(page, pageSize, start, limit);
|
|
348
|
+
|
|
349
|
+
if (!isNil(page)) {
|
|
350
|
+
query.page = convertPageQueryParams(page);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!isNil(pageSize)) {
|
|
354
|
+
query.pageSize = convertPageSizeQueryParams(pageSize, page);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isNil(start)) {
|
|
358
|
+
query.offset = convertStartQueryParams(start);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!isNil(limit)) {
|
|
362
|
+
query.limit = convertLimitQueryParams(limit);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
convertPublicationStateParams(schema, subPopulate, query);
|
|
366
|
+
|
|
251
367
|
return query;
|
|
252
368
|
};
|
|
253
369
|
|
|
@@ -377,6 +493,57 @@ const convertPublicationStateParams = (type, params = {}, query = {}) => {
|
|
|
377
493
|
}
|
|
378
494
|
};
|
|
379
495
|
|
|
496
|
+
const transformParamsToQuery = (uid, params) => {
|
|
497
|
+
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
|
|
498
|
+
const schema = strapi.getModel(uid);
|
|
499
|
+
|
|
500
|
+
const query = {};
|
|
501
|
+
|
|
502
|
+
const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
|
|
503
|
+
|
|
504
|
+
if (!isNil(_q)) {
|
|
505
|
+
query._q = _q;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!isNil(sort)) {
|
|
509
|
+
query.orderBy = convertSortQueryParams(sort);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!isNil(filters)) {
|
|
513
|
+
query.where = convertFiltersQueryParams(filters, schema);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!isNil(fields)) {
|
|
517
|
+
query.select = convertFieldsQueryParams(fields);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!isNil(populate)) {
|
|
521
|
+
query.populate = convertPopulateQueryParams(populate, schema);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
validatePaginationParams(page, pageSize, start, limit);
|
|
525
|
+
|
|
526
|
+
if (!isNil(page)) {
|
|
527
|
+
query.page = convertPageQueryParams(page);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!isNil(pageSize)) {
|
|
531
|
+
query.pageSize = convertPageSizeQueryParams(pageSize, page);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!isNil(start)) {
|
|
535
|
+
query.offset = convertStartQueryParams(start);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!isNil(limit)) {
|
|
539
|
+
query.limit = convertLimitQueryParams(limit);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
convertPublicationStateParams(schema, params, query);
|
|
543
|
+
|
|
544
|
+
return query;
|
|
545
|
+
};
|
|
546
|
+
|
|
380
547
|
module.exports = {
|
|
381
548
|
convertSortQueryParams,
|
|
382
549
|
convertStartQueryParams,
|
|
@@ -385,4 +552,5 @@ module.exports = {
|
|
|
385
552
|
convertFiltersQueryParams,
|
|
386
553
|
convertFieldsQueryParams,
|
|
387
554
|
convertPublicationStateParams,
|
|
555
|
+
transformParamsToQuery,
|
|
388
556
|
};
|
package/lib/errors.js
CHANGED
|
@@ -55,19 +55,28 @@ class ForbiddenError extends ApplicationError {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
class
|
|
58
|
+
class UnauthorizedError extends ApplicationError {
|
|
59
59
|
constructor(message, details) {
|
|
60
60
|
super(message, details);
|
|
61
|
-
this.name = '
|
|
62
|
-
this.message = message || '
|
|
61
|
+
this.name = 'UnauthorizedError';
|
|
62
|
+
this.message = message || 'Unauthorized';
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
class
|
|
66
|
+
class RateLimitError extends ApplicationError {
|
|
67
67
|
constructor(message, details) {
|
|
68
68
|
super(message, details);
|
|
69
|
-
this.name = '
|
|
70
|
-
this.message = message || '
|
|
69
|
+
this.name = 'RateLimitError';
|
|
70
|
+
this.message = message || 'Too many requests, please try again later.';
|
|
71
|
+
this.details = details || {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class PayloadTooLargeError extends ApplicationError {
|
|
76
|
+
constructor(message, details) {
|
|
77
|
+
super(message, details);
|
|
78
|
+
this.name = 'PayloadTooLargeError';
|
|
79
|
+
this.message = message || 'Entity too large';
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -88,7 +97,8 @@ module.exports = {
|
|
|
88
97
|
PaginationError,
|
|
89
98
|
NotFoundError,
|
|
90
99
|
ForbiddenError,
|
|
91
|
-
PayloadTooLargeError,
|
|
92
100
|
UnauthorizedError,
|
|
101
|
+
RateLimitError,
|
|
102
|
+
PayloadTooLargeError,
|
|
93
103
|
PolicyError,
|
|
94
104
|
};
|
package/lib/file.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utils file containing file treatment utils
|
|
5
|
+
*/
|
|
6
|
+
const { Writable } = require('stream');
|
|
7
|
+
|
|
8
|
+
const kbytesToBytes = (kbytes) => kbytes * 1000;
|
|
9
|
+
const bytesToKbytes = (bytes) => Math.round((bytes / 1000) * 100) / 100;
|
|
10
|
+
const bytesToHumanReadable = (bytes) => {
|
|
11
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
12
|
+
if (bytes === 0) return '0 Bytes';
|
|
13
|
+
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)), 10);
|
|
14
|
+
return `${Math.round(bytes / 1000 ** i, 2)} ${sizes[i]}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const streamToBuffer = (stream) =>
|
|
18
|
+
new Promise((resolve, reject) => {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
stream.on('data', (chunk) => {
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
});
|
|
23
|
+
stream.on('end', () => {
|
|
24
|
+
resolve(Buffer.concat(chunks));
|
|
25
|
+
});
|
|
26
|
+
stream.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const getStreamSize = (stream) =>
|
|
30
|
+
new Promise((resolve, reject) => {
|
|
31
|
+
let size = 0;
|
|
32
|
+
stream.on('data', (chunk) => {
|
|
33
|
+
size += Buffer.byteLength(chunk);
|
|
34
|
+
});
|
|
35
|
+
stream.on('close', () => resolve(size));
|
|
36
|
+
stream.on('error', reject);
|
|
37
|
+
stream.resume();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a writeable Node.js stream that discards received data.
|
|
42
|
+
* Useful for testing, draining a stream of data, etc.
|
|
43
|
+
*/
|
|
44
|
+
function writableDiscardStream(options) {
|
|
45
|
+
return new Writable({
|
|
46
|
+
...options,
|
|
47
|
+
write(chunk, encding, callback) {
|
|
48
|
+
setImmediate(callback);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
streamToBuffer,
|
|
55
|
+
bytesToHumanReadable,
|
|
56
|
+
bytesToKbytes,
|
|
57
|
+
kbytesToBytes,
|
|
58
|
+
getStreamSize,
|
|
59
|
+
writableDiscardStream,
|
|
60
|
+
};
|
package/lib/hooks.js
CHANGED
|
@@ -2,17 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { eq, remove, cloneDeep } = require('lodash/fp');
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @typedef Hook
|
|
7
|
-
* @property {Array<Function>} _handlers - A registry of handler used by the hook
|
|
8
|
-
* @property {function(Function):Hook} register - Register a new handler into the hook's registry
|
|
9
|
-
* @property {function(Function):Hook} delete- Delete the given handler from the hook's registry
|
|
10
|
-
* @property {Function} call - Not implemented by default, can be replaced by any implementation.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
5
|
/**
|
|
14
6
|
* Create a default Strapi hook
|
|
15
|
-
* @return {Hook}
|
|
16
7
|
*/
|
|
17
8
|
const createHook = () => {
|
|
18
9
|
const state = {
|
|
@@ -45,7 +36,6 @@ const createHook = () => {
|
|
|
45
36
|
/**
|
|
46
37
|
* Create an async series hook.
|
|
47
38
|
* Upon execution, it will execute every handler in order with the same context
|
|
48
|
-
* @return {Hook}
|
|
49
39
|
*/
|
|
50
40
|
const createAsyncSeriesHook = () => ({
|
|
51
41
|
...createHook(),
|
|
@@ -60,7 +50,6 @@ const createAsyncSeriesHook = () => ({
|
|
|
60
50
|
/**
|
|
61
51
|
* Create an async series waterfall hook.
|
|
62
52
|
* Upon execution, it will execute every handler in order and pass the return value of the last handler to the next one
|
|
63
|
-
* @return {Hook}
|
|
64
53
|
*/
|
|
65
54
|
const createAsyncSeriesWaterfallHook = () => ({
|
|
66
55
|
...createHook(),
|
|
@@ -79,7 +68,6 @@ const createAsyncSeriesWaterfallHook = () => ({
|
|
|
79
68
|
/**
|
|
80
69
|
* Create an async parallel hook.
|
|
81
70
|
* Upon execution, it will execute every registered handler in band.
|
|
82
|
-
* @return {Hook}
|
|
83
71
|
*/
|
|
84
72
|
const createAsyncParallelHook = () => ({
|
|
85
73
|
...createHook(),
|
|
@@ -91,6 +79,24 @@ const createAsyncParallelHook = () => ({
|
|
|
91
79
|
},
|
|
92
80
|
});
|
|
93
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Create an async parallel hook.
|
|
84
|
+
* Upon execution, it will execute every registered handler in serie and return the first result found.
|
|
85
|
+
*/
|
|
86
|
+
const createAsyncBailHook = () => ({
|
|
87
|
+
...createHook(),
|
|
88
|
+
|
|
89
|
+
async call(context) {
|
|
90
|
+
for (const handler of this.getHandlers()) {
|
|
91
|
+
const result = await handler(context);
|
|
92
|
+
|
|
93
|
+
if (result !== undefined) {
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
94
100
|
module.exports = {
|
|
95
101
|
// Internal utils
|
|
96
102
|
internals: {
|
|
@@ -100,4 +106,5 @@ module.exports = {
|
|
|
100
106
|
createAsyncSeriesHook,
|
|
101
107
|
createAsyncSeriesWaterfallHook,
|
|
102
108
|
createAsyncParallelHook,
|
|
109
|
+
createAsyncBailHook,
|
|
103
110
|
};
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ const {
|
|
|
24
24
|
joinBy,
|
|
25
25
|
toKebabCase,
|
|
26
26
|
} = require('./string-formatting');
|
|
27
|
-
const { removeUndefined } = require('./object-formatting');
|
|
27
|
+
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');
|
|
@@ -37,7 +37,11 @@ const providerFactory = require('./provider-factory');
|
|
|
37
37
|
const pagination = require('./pagination');
|
|
38
38
|
const sanitize = require('./sanitize');
|
|
39
39
|
const traverseEntity = require('./traverse-entity');
|
|
40
|
-
const pipeAsync = require('./
|
|
40
|
+
const { pipeAsync, mapAsync, forEachAsync } = require('./async');
|
|
41
|
+
const convertQueryParams = require('./convert-query-params');
|
|
42
|
+
const importDefault = require('./import-default');
|
|
43
|
+
const template = require('./template');
|
|
44
|
+
const file = require('./file');
|
|
41
45
|
|
|
42
46
|
module.exports = {
|
|
43
47
|
yup,
|
|
@@ -59,11 +63,13 @@ module.exports = {
|
|
|
59
63
|
getConfigUrls,
|
|
60
64
|
escapeQuery,
|
|
61
65
|
removeUndefined,
|
|
66
|
+
keysDeep,
|
|
62
67
|
getAbsoluteAdminUrl,
|
|
63
68
|
getAbsoluteServerUrl,
|
|
64
69
|
generateTimestampCode,
|
|
65
70
|
stringIncludes,
|
|
66
71
|
stringEquals,
|
|
72
|
+
template,
|
|
67
73
|
isKebabCase,
|
|
68
74
|
isCamelCase,
|
|
69
75
|
toKebabCase,
|
|
@@ -76,7 +82,12 @@ module.exports = {
|
|
|
76
82
|
providerFactory,
|
|
77
83
|
pagination,
|
|
78
84
|
pipeAsync,
|
|
85
|
+
mapAsync,
|
|
86
|
+
forEachAsync,
|
|
79
87
|
errors,
|
|
80
88
|
validateYupSchema,
|
|
81
89
|
validateYupSchemaSync,
|
|
90
|
+
convertQueryParams,
|
|
91
|
+
importDefault,
|
|
92
|
+
file,
|
|
82
93
|
};
|
package/lib/object-formatting.js
CHANGED
|
@@ -4,6 +4,12 @@ const _ = require('lodash');
|
|
|
4
4
|
|
|
5
5
|
const removeUndefined = (obj) => _.pickBy(obj, (value) => typeof value !== 'undefined');
|
|
6
6
|
|
|
7
|
+
const keysDeep = (obj, path = []) =>
|
|
8
|
+
!_.isObject(obj)
|
|
9
|
+
? path.join('.')
|
|
10
|
+
: _.reduce(obj, (acc, next, key) => _.concat(acc, keysDeep(next, [...path, key])), []);
|
|
11
|
+
|
|
7
12
|
module.exports = {
|
|
8
13
|
removeUndefined,
|
|
14
|
+
keysDeep,
|
|
9
15
|
};
|
package/lib/print-value.js
CHANGED
|
@@ -11,6 +11,7 @@ const symbolToString = typeof Symbol !== 'undefined' ? Symbol.prototype.toString
|
|
|
11
11
|
const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/;
|
|
12
12
|
|
|
13
13
|
function printNumber(val) {
|
|
14
|
+
// eslint-disable-next-line eqeqeq
|
|
14
15
|
if (val != +val) return 'NaN';
|
|
15
16
|
const isNegativeZero = val === 0 && 1 / val < 0;
|
|
16
17
|
return isNegativeZero ? '-0' : `${val}`;
|
package/lib/provider-factory.js
CHANGED
|
@@ -25,25 +25,9 @@ const createProviderHooksMap = () => ({
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* A
|
|
29
|
-
* @typedef {Object} Provider
|
|
30
|
-
* @property hooks
|
|
31
|
-
* @property register
|
|
32
|
-
* @property delete
|
|
33
|
-
* @property get
|
|
34
|
-
* @property getWhere
|
|
35
|
-
* @property values
|
|
36
|
-
* @property keys
|
|
37
|
-
* @property has
|
|
38
|
-
* @property size
|
|
39
|
-
* @property clear
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A {@link Provider} factory
|
|
28
|
+
* A Provider factory
|
|
44
29
|
* @param {Object} [options] - The factory options
|
|
45
30
|
* @param {boolean = true} options.throwOnDuplicates - Specify the wanted behaviour when encountering a duplicate key on register
|
|
46
|
-
* @return {Provider}
|
|
47
31
|
*/
|
|
48
32
|
const providerFactory = (options = {}) => {
|
|
49
33
|
const { throwOnDuplicates = true } = options;
|
package/lib/relations.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { isRelationalAttribute } = require('./content-types');
|
|
4
|
+
|
|
3
5
|
const MANY_RELATIONS = ['oneToMany', 'manyToMany'];
|
|
4
6
|
|
|
5
7
|
const getRelationalFields = (contentType) => {
|
|
@@ -8,8 +10,21 @@ const getRelationalFields = (contentType) => {
|
|
|
8
10
|
});
|
|
9
11
|
};
|
|
10
12
|
|
|
13
|
+
const isOneToAny = (attribute) =>
|
|
14
|
+
isRelationalAttribute(attribute) && ['oneToOne', 'oneToMany'].includes(attribute.relation);
|
|
15
|
+
const isManyToAny = (attribute) =>
|
|
16
|
+
isRelationalAttribute(attribute) && ['manyToMany', 'manyToOne'].includes(attribute.relation);
|
|
17
|
+
const isAnyToOne = (attribute) =>
|
|
18
|
+
isRelationalAttribute(attribute) && ['oneToOne', 'manyToOne'].includes(attribute.relation);
|
|
19
|
+
const isAnyToMany = (attribute) =>
|
|
20
|
+
isRelationalAttribute(attribute) && ['oneToMany', 'manyToMany'].includes(attribute.relation);
|
|
21
|
+
|
|
11
22
|
module.exports = {
|
|
12
23
|
getRelationalFields,
|
|
24
|
+
isOneToAny,
|
|
25
|
+
isManyToAny,
|
|
26
|
+
isAnyToOne,
|
|
27
|
+
isAnyToMany,
|
|
13
28
|
constants: {
|
|
14
29
|
MANY_RELATIONS,
|
|
15
30
|
},
|
package/lib/sanitize/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const { isArray } = require('lodash/fp');
|
|
|
4
4
|
|
|
5
5
|
const traverseEntity = require('../traverse-entity');
|
|
6
6
|
const { getNonWritableAttributes } = require('../content-types');
|
|
7
|
-
const pipeAsync = require('../
|
|
7
|
+
const { pipeAsync } = require('../async');
|
|
8
8
|
|
|
9
9
|
const visitors = require('./visitors');
|
|
10
10
|
const sanitizers = require('./sanitizers');
|
package/lib/template.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a strict interpolation RegExp based on the given variables' name
|
|
5
|
+
*
|
|
6
|
+
* @param {string[]} allowedVariableNames - The list of allowed variables
|
|
7
|
+
* @param {string} [flags] - The RegExp flags
|
|
8
|
+
*/
|
|
9
|
+
const createStrictInterpolationRegExp = (allowedVariableNames, flags) => {
|
|
10
|
+
const oneOfVariables = allowedVariableNames.join('|');
|
|
11
|
+
|
|
12
|
+
// 1. We need to match the delimiters: <%= ... %>
|
|
13
|
+
// 2. We accept any number of whitespaces characters before and/or after the variable name: \s* ... \s*
|
|
14
|
+
// 3. We only accept values from the variable list as interpolation variables' name: : (${oneOfVariables})
|
|
15
|
+
return new RegExp(`<%=\\s*(${oneOfVariables})\\s*%>`, flags);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a loose interpolation RegExp to match as many groups as possible
|
|
20
|
+
*
|
|
21
|
+
* @param {string} [flags] - The RegExp flags
|
|
22
|
+
*/
|
|
23
|
+
const createLooseInterpolationRegExp = (flags) => new RegExp(/<%=([\s\S]+?)%>/, flags);
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
createStrictInterpolationRegExp,
|
|
27
|
+
createLooseInterpolationRegExp,
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/utils",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-ffd0a215143bcafd0354d791533a57a084e53ffb",
|
|
4
4
|
"description": "Shared utilities for the Strapi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -36,14 +36,15 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@sindresorhus/slugify": "1.1.0",
|
|
39
|
-
"date-fns": "2.29.
|
|
39
|
+
"date-fns": "2.29.3",
|
|
40
40
|
"http-errors": "1.8.1",
|
|
41
41
|
"lodash": "4.17.21",
|
|
42
|
+
"p-map": "4.0.0",
|
|
42
43
|
"yup": "0.32.9"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
45
|
-
"node": ">=14.19.1 <=
|
|
46
|
+
"node": ">=14.19.1 <=18.x.x",
|
|
46
47
|
"npm": ">=6.0.0"
|
|
47
48
|
},
|
|
48
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "ffd0a215143bcafd0354d791533a57a084e53ffb"
|
|
49
50
|
}
|