mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 10:55:37 +00:00
wip
This commit is contained in:
parent
f108e049e8
commit
ae0850295d
@ -1,6 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
import { has } from 'lodash/fp';
|
||||
import type { Model, Kind, Attribute, RelationalAttribute } from './types';
|
||||
import type {
|
||||
Model,
|
||||
Kind,
|
||||
Attribute,
|
||||
RelationalAttribute,
|
||||
ComponentAttribute,
|
||||
DynamicZoneAttribute,
|
||||
} from './types';
|
||||
|
||||
const SINGLE_TYPE = 'singleType';
|
||||
const COLLECTION_TYPE = 'collectionType';
|
||||
@ -113,10 +120,11 @@ const isMediaAttribute = (attribute: Attribute) => attribute?.type === 'media';
|
||||
const isRelationalAttribute = (attribute: Attribute): attribute is RelationalAttribute =>
|
||||
attribute?.type === 'relation';
|
||||
|
||||
const isComponentAttribute = (attribute: Attribute) =>
|
||||
const isComponentAttribute = (attribute: Attribute): attribute is ComponentAttribute =>
|
||||
['component', 'dynamiczone'].includes(attribute?.type);
|
||||
|
||||
const isDynamicZoneAttribute = (attribute: Attribute) => attribute?.type === 'dynamiczone';
|
||||
const isDynamicZoneAttribute = (attribute: Attribute): attribute is DynamicZoneAttribute =>
|
||||
attribute?.type === 'dynamiczone';
|
||||
const isMorphToRelationalAttribute = (attribute: Attribute) => {
|
||||
return isRelationalAttribute(attribute) && attribute?.relation?.startsWith?.('morphTo');
|
||||
};
|
||||
|
||||
@ -1,36 +1,88 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts the standard Strapi REST query params to a more usable format for querying
|
||||
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
||||
*/
|
||||
|
||||
const {
|
||||
import {
|
||||
isNil,
|
||||
toNumber,
|
||||
isInteger,
|
||||
has,
|
||||
isEmpty,
|
||||
isObject,
|
||||
isPlainObject,
|
||||
cloneDeep,
|
||||
get,
|
||||
mergeAll,
|
||||
} = require('lodash/fp');
|
||||
const _ = require('lodash');
|
||||
const parseType = require('./parse-type');
|
||||
const contentTypesUtils = require('./content-types');
|
||||
const { PaginationError } = require('./errors');
|
||||
const {
|
||||
isArray,
|
||||
isString,
|
||||
} from 'lodash/fp';
|
||||
import _ from 'lodash';
|
||||
import parseType from './parse-type';
|
||||
import * as contentTypesUtils from './content-types';
|
||||
import { PaginationError } from './errors';
|
||||
import {
|
||||
isMediaAttribute,
|
||||
isDynamicZoneAttribute,
|
||||
isMorphToRelationalAttribute,
|
||||
} = require('./content-types');
|
||||
} from './content-types';
|
||||
import { Model } from './types';
|
||||
|
||||
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
||||
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
|
||||
interface SortMap {
|
||||
[key: string]: SortOrder | SortMap;
|
||||
}
|
||||
|
||||
type SortQuery = string | string[] | object;
|
||||
|
||||
type FieldsQuery = string | string[];
|
||||
interface FiltersQuery {}
|
||||
|
||||
type PopulateParams = {
|
||||
sort?: SortQuery;
|
||||
fields?: FieldsQuery;
|
||||
filters?: FiltersQuery;
|
||||
populate?: PopulateQuery;
|
||||
on: {
|
||||
[key: string]: PopulateParams;
|
||||
};
|
||||
};
|
||||
|
||||
type PopulateQuery = string | string[] | PopulateParams;
|
||||
|
||||
interface Query {
|
||||
sort?: SortQuery;
|
||||
fields?: FieldsQuery;
|
||||
filters?: FiltersQuery;
|
||||
populate?: PopulateQuery;
|
||||
count: boolean;
|
||||
ordering: unknown;
|
||||
_q?: string;
|
||||
limit?: number | string;
|
||||
start?: number | string;
|
||||
page?: number | string;
|
||||
pageSize?: number | string;
|
||||
}
|
||||
|
||||
interface ConvertedQuery {
|
||||
orderBy?: SortQuery;
|
||||
select?: FieldsQuery;
|
||||
where?: FiltersQuery;
|
||||
populate?: PopulateQuery;
|
||||
count: boolean;
|
||||
ordering: unknown;
|
||||
_q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
class InvalidOrderError extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
@ -45,41 +97,47 @@ class InvalidSortError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const validateOrder = (order) => {
|
||||
function validateOrder(order: string): asserts order is SortOrder {
|
||||
if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) {
|
||||
throw new InvalidOrderError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const convertCountQueryParams = (countQuery) => {
|
||||
const convertCountQueryParams = (countQuery: unknown): boolean => {
|
||||
return parseType({ type: 'boolean', value: countQuery });
|
||||
};
|
||||
|
||||
const convertOrderingQueryParams = (ordering) => {
|
||||
const convertOrderingQueryParams = (ordering: unknown) => {
|
||||
return ordering;
|
||||
};
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => _.isPlainObject(value);
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
isArray(value) && value.every(isString);
|
||||
|
||||
/**
|
||||
* Sort query parser
|
||||
* @param {string} sortQuery - ex: id:asc,price:desc
|
||||
*/
|
||||
const convertSortQueryParams = (sortQuery) => {
|
||||
const convertSortQueryParams = (sortQuery: SortQuery): SortMap | SortMap[] => {
|
||||
if (typeof sortQuery === 'string') {
|
||||
return sortQuery.split(',').map((value) => convertSingleSortQueryParam(value));
|
||||
return convertStringSortQueryParam(sortQuery);
|
||||
}
|
||||
if (isStringArray(sortQuery)) {
|
||||
return sortQuery.flatMap((sortValue: string) => convertStringSortQueryParam(sortValue));
|
||||
}
|
||||
|
||||
if (Array.isArray(sortQuery)) {
|
||||
return sortQuery.flatMap((sortValue) => convertSortQueryParams(sortValue));
|
||||
}
|
||||
|
||||
if (_.isPlainObject(sortQuery)) {
|
||||
if (isPlainObject(sortQuery)) {
|
||||
return convertNestedSortQueryParam(sortQuery);
|
||||
}
|
||||
|
||||
throw new InvalidSortError();
|
||||
};
|
||||
|
||||
const convertSingleSortQueryParam = (sortQuery) => {
|
||||
const convertStringSortQueryParam = (sortQuery: string): SortMap[] => {
|
||||
return sortQuery.split(',').map((value) => convertSingleSortQueryParam(value));
|
||||
};
|
||||
|
||||
const convertSingleSortQueryParam = (sortQuery: string): SortMap => {
|
||||
// split field and order param with default order to ascending
|
||||
const [field, order = 'asc'] = sortQuery.split(':');
|
||||
|
||||
@ -92,17 +150,19 @@ const convertSingleSortQueryParam = (sortQuery) => {
|
||||
return _.set({}, field, order);
|
||||
};
|
||||
|
||||
const convertNestedSortQueryParam = (sortQuery) => {
|
||||
const transformedSort = {};
|
||||
const convertNestedSortQueryParam = (sortQuery: Record<string, unknown>): SortMap => {
|
||||
const transformedSort: SortMap = {};
|
||||
for (const field of Object.keys(sortQuery)) {
|
||||
const order = sortQuery[field];
|
||||
|
||||
// this is a deep sort
|
||||
if (_.isPlainObject(order)) {
|
||||
if (isPlainObject(order)) {
|
||||
transformedSort[field] = convertNestedSortQueryParam(order);
|
||||
} else {
|
||||
} else if (typeof order === 'string') {
|
||||
validateOrder(order);
|
||||
transformedSort[field] = order;
|
||||
} else {
|
||||
throw Error(`Invalid sort type expected object or string got ${typeof order}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,9 +171,8 @@ const convertNestedSortQueryParam = (sortQuery) => {
|
||||
|
||||
/**
|
||||
* Start query parser
|
||||
* @param {string} startQuery
|
||||
*/
|
||||
const convertStartQueryParams = (startQuery) => {
|
||||
const convertStartQueryParams = (startQuery: unknown): number => {
|
||||
const startAsANumber = _.toNumber(startQuery);
|
||||
|
||||
if (!_.isInteger(startAsANumber) || startAsANumber < 0) {
|
||||
@ -125,21 +184,22 @@ const convertStartQueryParams = (startQuery) => {
|
||||
|
||||
/**
|
||||
* Limit query parser
|
||||
* @param {string} limitQuery
|
||||
*/
|
||||
const convertLimitQueryParams = (limitQuery) => {
|
||||
const convertLimitQueryParams = (limitQuery: unknown): number | undefined => {
|
||||
const limitAsANumber = _.toNumber(limitQuery);
|
||||
|
||||
if (!_.isInteger(limitAsANumber) || (limitAsANumber !== -1 && limitAsANumber < 0)) {
|
||||
throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
|
||||
}
|
||||
|
||||
if (limitAsANumber === -1) return null;
|
||||
if (limitAsANumber === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return limitAsANumber;
|
||||
};
|
||||
|
||||
const convertPageQueryParams = (page) => {
|
||||
const convertPageQueryParams = (page: unknown): number => {
|
||||
const pageVal = toNumber(page);
|
||||
|
||||
if (!isInteger(pageVal) || pageVal <= 0) {
|
||||
@ -151,7 +211,7 @@ const convertPageQueryParams = (page) => {
|
||||
return pageVal;
|
||||
};
|
||||
|
||||
const convertPageSizeQueryParams = (pageSize, page) => {
|
||||
const convertPageSizeQueryParams = (pageSize: unknown, page: unknown): number => {
|
||||
const pageSizeVal = toNumber(pageSize);
|
||||
|
||||
if (!isInteger(pageSizeVal) || pageSizeVal <= 0) {
|
||||
@ -163,7 +223,12 @@ const convertPageSizeQueryParams = (pageSize, page) => {
|
||||
return pageSizeVal;
|
||||
};
|
||||
|
||||
const validatePaginationParams = (page, pageSize, start, limit) => {
|
||||
const validatePaginationParams = (
|
||||
page: unknown,
|
||||
pageSize: unknown,
|
||||
start: unknown,
|
||||
limit: unknown
|
||||
) => {
|
||||
const isPagePagination = !isNil(page) || !isNil(pageSize);
|
||||
const isOffsetPagination = !isNil(start) || !isNil(limit);
|
||||
|
||||
@ -183,7 +248,7 @@ class InvalidPopulateError extends Error {
|
||||
}
|
||||
|
||||
// NOTE: we could support foo.* or foo.bar.* etc later on
|
||||
const convertPopulateQueryParams = (populate, schema, depth = 0) => {
|
||||
const convertPopulateQueryParams = (populate: PopulateQuery, schema: Model, depth = 0) => {
|
||||
if (depth === 0 && populate === '*') {
|
||||
return true;
|
||||
}
|
||||
@ -212,7 +277,7 @@ const convertPopulateQueryParams = (populate, schema, depth = 0) => {
|
||||
throw new InvalidPopulateError();
|
||||
};
|
||||
|
||||
const convertPopulateObject = (populate, schema) => {
|
||||
const convertPopulateObject = (populate: PopulateParams, schema: Model) => {
|
||||
if (!schema) {
|
||||
return {};
|
||||
}
|
||||
@ -252,7 +317,7 @@ const convertPopulateObject = (populate, schema) => {
|
||||
|
||||
// TODO: This is a query's populate fallback for DynamicZone and is kept for legacy purpose.
|
||||
// Removing it could break existing user queries but it should be removed in V5.
|
||||
if (attribute.type === 'dynamiczone') {
|
||||
if (isDynamicZoneAttribute(attribute)) {
|
||||
const populates = attribute.components
|
||||
.map((uid) => strapi.getModel(uid))
|
||||
.map((schema) => convertNestedPopulate(subPopulate, schema))
|
||||
@ -303,7 +368,7 @@ const convertPopulateObject = (populate, schema) => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const convertNestedPopulate = (subPopulate, schema) => {
|
||||
const convertNestedPopulate = (subPopulate: PopulateQuery, schema: Model) => {
|
||||
if (_.isString(subPopulate)) {
|
||||
return parseType({ type: 'boolean', value: subPopulate, forceCast: true });
|
||||
}
|
||||
@ -319,7 +384,7 @@ const convertNestedPopulate = (subPopulate, schema) => {
|
||||
const { sort, filters, fields, populate, count, ordering, page, pageSize, start, limit } =
|
||||
subPopulate;
|
||||
|
||||
const query = {};
|
||||
const query: ConvertedQuery = {};
|
||||
|
||||
if (sort) {
|
||||
query.orderBy = convertSortQueryParams(sort);
|
||||
@ -368,7 +433,7 @@ const convertNestedPopulate = (subPopulate, schema) => {
|
||||
return query;
|
||||
};
|
||||
|
||||
const convertFieldsQueryParams = (fields, depth = 0) => {
|
||||
const convertFieldsQueryParams = (fields: FieldsQuery, depth = 0): string[] | undefined => {
|
||||
if (depth === 0 && fields === '*') {
|
||||
return undefined;
|
||||
}
|
||||
@ -378,16 +443,19 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
|
||||
return _.uniq(['id', ...fieldsValues]);
|
||||
}
|
||||
|
||||
if (Array.isArray(fields)) {
|
||||
if (isStringArray(fields)) {
|
||||
// map convert
|
||||
const fieldsValues = fields.flatMap((value) => convertFieldsQueryParams(value, depth + 1));
|
||||
const fieldsValues = fields
|
||||
.flatMap((value) => convertFieldsQueryParams(value, depth + 1))
|
||||
.filter((v) => !isNil(v)) as string[];
|
||||
|
||||
return _.uniq(['id', ...fieldsValues]);
|
||||
}
|
||||
|
||||
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
|
||||
};
|
||||
|
||||
const convertFiltersQueryParams = (filters, schema) => {
|
||||
const convertFiltersQueryParams = (filters: FiltersQuery, schema: Model) => {
|
||||
// Filters need to be either an array or an object
|
||||
// Here we're only checking for 'object' type since typeof [] => object and typeof {} => object
|
||||
if (!isObject(filters)) {
|
||||
@ -400,7 +468,7 @@ const convertFiltersQueryParams = (filters, schema) => {
|
||||
return convertAndSanitizeFilters(filtersCopy, schema);
|
||||
};
|
||||
|
||||
const convertAndSanitizeFilters = (filters, schema) => {
|
||||
const convertAndSanitizeFilters = (filters: FiltersQuery, schema: Model) => {
|
||||
if (!isPlainObject(filters)) {
|
||||
return filters;
|
||||
}
|
||||
@ -415,7 +483,7 @@ const convertAndSanitizeFilters = (filters, schema) => {
|
||||
);
|
||||
}
|
||||
|
||||
const removeOperator = (operator) => delete filters[operator];
|
||||
const removeOperator = (operator: string) => delete filters[operator];
|
||||
|
||||
// Here, `key` can either be an operator or an attribute name
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
@ -494,11 +562,11 @@ const convertPublicationStateParams = (type, params = {}, query = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
const transformParamsToQuery = (uid, params) => {
|
||||
const transformParamsToQuery = (uid: string, params: Query) => {
|
||||
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
|
||||
const schema = strapi.getModel(uid);
|
||||
|
||||
const query = {};
|
||||
const query: ConvertedQuery = {};
|
||||
|
||||
const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
|
||||
|
||||
@ -545,7 +613,7 @@ const transformParamsToQuery = (uid, params) => {
|
||||
return query;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export = {
|
||||
convertSortQueryParams,
|
||||
convertStartQueryParams,
|
||||
convertLimitQueryParams,
|
||||
@ -7,7 +7,7 @@ const isDate = (v: unknown): v is Date => {
|
||||
return dates.isDate(v);
|
||||
};
|
||||
|
||||
const parseTime = (value: string | Date) => {
|
||||
const parseTime = (value: string | Date): string => {
|
||||
if (isDate(value)) {
|
||||
return dates.format(value, 'HH:mm:ss.SSS');
|
||||
}
|
||||
@ -61,8 +61,20 @@ const parseDateTimeOrTimestamp = (value: string | Date) => {
|
||||
}
|
||||
};
|
||||
|
||||
type TypeMap = {
|
||||
boolean: boolean;
|
||||
integer: number;
|
||||
biginteger: number;
|
||||
float: number;
|
||||
decimal: number;
|
||||
time: string;
|
||||
date: string;
|
||||
timestamp: Date;
|
||||
datetime: Date;
|
||||
};
|
||||
|
||||
interface ParseTypeOptions {
|
||||
type: string;
|
||||
type: keyof TypeMap;
|
||||
value: any;
|
||||
forceCast?: boolean;
|
||||
}
|
||||
@ -70,10 +82,14 @@ interface ParseTypeOptions {
|
||||
/**
|
||||
* Cast basic values based on attribute type
|
||||
*/
|
||||
const parseType = ({ type, value, forceCast = false }: ParseTypeOptions) => {
|
||||
const parseType = (options: ParseTypeOptions) => {
|
||||
const { type, value, forceCast = false } = options;
|
||||
|
||||
switch (type) {
|
||||
case 'boolean': {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (['true', 't', '1', 1].includes(value)) {
|
||||
return true;
|
||||
@ -110,4 +126,4 @@ const parseType = ({ type, value, forceCast = false }: ParseTypeOptions) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = parseType;
|
||||
export default parseType;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { CurriedFunction1 } from 'lodash';
|
||||
import { isArray, cloneDeep } from 'lodash/fp';
|
||||
|
||||
import { getNonWritableAttributes } from '../content-types';
|
||||
@ -5,13 +6,24 @@ import { pipeAsync } from '../async';
|
||||
|
||||
import * as visitors from './visitors';
|
||||
import * as sanitizers from './sanitizers';
|
||||
import traverseEntity from '../traverse-entity';
|
||||
import traverseEntity, { Data } from '../traverse-entity';
|
||||
|
||||
import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from '../traverse';
|
||||
import { Model } from '../types';
|
||||
|
||||
interface Options {
|
||||
auth?: unknown;
|
||||
}
|
||||
|
||||
interface Sanitizer {
|
||||
(schema: Model): CurriedFunction1<Data, Promise<Data>>;
|
||||
}
|
||||
export interface SanitizeFunc {
|
||||
(data: unknown, schema: Model, options?: Options): Promise<unknown>;
|
||||
}
|
||||
|
||||
const createContentAPISanitizers = () => {
|
||||
const sanitizeInput = (data, schema: Model, { auth } = {}) => {
|
||||
const sanitizeInput: SanitizeFunc = (data: unknown, schema: Model, { auth } = {}) => {
|
||||
if (isArray(data)) {
|
||||
return Promise.all(data.map((entry) => sanitizeInput(entry, schema, { auth })));
|
||||
}
|
||||
@ -31,12 +43,12 @@ const createContentAPISanitizers = () => {
|
||||
// Apply sanitizers from registry if exists
|
||||
strapi.sanitizers
|
||||
.get('content-api.input')
|
||||
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
||||
.forEach((sanitizer: Sanitizer) => transforms.push(sanitizer(schema)));
|
||||
|
||||
return pipeAsync(...transforms)(data);
|
||||
};
|
||||
|
||||
const sanitizeOutput = async (data, schema: Model, { auth } = {}) => {
|
||||
const sanitizeOutput: SanitizeFunc = async (data, schema: Model, { auth } = {}) => {
|
||||
if (isArray(data)) {
|
||||
const res = new Array(data.length);
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
@ -45,7 +57,7 @@ const createContentAPISanitizers = () => {
|
||||
return res;
|
||||
}
|
||||
|
||||
const transforms = [(data) => sanitizers.defaultSanitizeOutput(schema, data)];
|
||||
const transforms = [(data: Data) => sanitizers.defaultSanitizeOutput(schema, data)];
|
||||
|
||||
if (auth) {
|
||||
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
||||
@ -54,12 +66,16 @@ const createContentAPISanitizers = () => {
|
||||
// Apply sanitizers from registry if exists
|
||||
strapi.sanitizers
|
||||
.get('content-api.output')
|
||||
.forEach((sanitizer) => transforms.push(sanitizer(schema)));
|
||||
.forEach((sanitizer: Sanitizer) => transforms.push(sanitizer(schema)));
|
||||
|
||||
return pipeAsync(...transforms)(data);
|
||||
};
|
||||
|
||||
const sanitizeQuery = async (query, schema, { auth } = {}) => {
|
||||
const sanitizeQuery = async (
|
||||
query: Record<string, unknown>,
|
||||
schema: Model,
|
||||
{ auth }: Options = {}
|
||||
) => {
|
||||
const { filters, sort, fields, populate } = query;
|
||||
|
||||
const sanitizedQuery = cloneDeep(query);
|
||||
@ -83,7 +99,7 @@ const createContentAPISanitizers = () => {
|
||||
return sanitizedQuery;
|
||||
};
|
||||
|
||||
const sanitizeFilters = (filters, schema: Model, { auth } = {}) => {
|
||||
const sanitizeFilters: SanitizeFunc = (filters, schema: Model, { auth } = {}) => {
|
||||
if (isArray(filters)) {
|
||||
return Promise.all(filters.map((filter) => sanitizeFilters(filter, schema, { auth })));
|
||||
}
|
||||
@ -97,7 +113,7 @@ const createContentAPISanitizers = () => {
|
||||
return pipeAsync(...transforms)(filters);
|
||||
};
|
||||
|
||||
const sanitizeSort = (sort, schema: Model, { auth } = {}) => {
|
||||
const sanitizeSort: SanitizeFunc = (sort, schema: Model, { auth } = {}) => {
|
||||
const transforms = [sanitizers.defaultSanitizeSort(schema)];
|
||||
|
||||
if (auth) {
|
||||
@ -107,13 +123,13 @@ const createContentAPISanitizers = () => {
|
||||
return pipeAsync(...transforms)(sort);
|
||||
};
|
||||
|
||||
const sanitizeFields = (fields, schema: Model) => {
|
||||
const sanitizeFields: SanitizeFunc = (fields, schema: Model) => {
|
||||
const transforms = [sanitizers.defaultSanitizeFields(schema)];
|
||||
|
||||
return pipeAsync(...transforms)(fields);
|
||||
};
|
||||
|
||||
const sanitizePopulate = (populate, schema: Model, { auth } = {}) => {
|
||||
const sanitizePopulate: SanitizeFunc = (populate, schema: Model, { auth } = {}) => {
|
||||
const transforms = [sanitizers.defaultSanitizePopulate(schema)];
|
||||
|
||||
if (auth) {
|
||||
|
||||
@ -35,7 +35,7 @@ const defaultSanitizeOutput = async (schema: Model, entity: Data) => {
|
||||
);
|
||||
};
|
||||
|
||||
const defaultSanitizeFilters = curry((schema: Model, filters) => {
|
||||
const defaultSanitizeFilters = curry((schema: Model, filters: unknown) => {
|
||||
return pipeAsync(
|
||||
// Remove dynamic zones from filters
|
||||
traverseQueryFilters(removeDynamicZones, { schema }),
|
||||
@ -57,7 +57,7 @@ const defaultSanitizeFilters = curry((schema: Model, filters) => {
|
||||
)(filters);
|
||||
});
|
||||
|
||||
const defaultSanitizeSort = curry((schema: Model, sort) => {
|
||||
const defaultSanitizeSort = curry((schema: Model, sort: unknown) => {
|
||||
return pipeAsync(
|
||||
// Remove non attribute keys
|
||||
traverseQuerySort(
|
||||
@ -94,11 +94,11 @@ const defaultSanitizeSort = curry((schema: Model, sort) => {
|
||||
)(sort);
|
||||
});
|
||||
|
||||
const defaultSanitizeFields = curry((schema: Model, fields) => {
|
||||
const defaultSanitizeFields = curry((schema: Model, fields: unknown) => {
|
||||
return pipeAsync(
|
||||
// Only keep scalar attributes
|
||||
traverseQueryFields(
|
||||
({ key, attribute }: , { remove }) => {
|
||||
({ key, attribute }, { remove }) => {
|
||||
if (isNil(attribute) || !isScalarAttribute(attribute)) {
|
||||
remove(key);
|
||||
}
|
||||
@ -114,7 +114,7 @@ const defaultSanitizeFields = curry((schema: Model, fields) => {
|
||||
)(fields);
|
||||
});
|
||||
|
||||
const defaultSanitizePopulate = curry((schema: Model, populate) => {
|
||||
const defaultSanitizePopulate = curry((schema: Model, populate: unknown) => {
|
||||
return pipeAsync(
|
||||
traverseQueryPopulate(
|
||||
async ({ key, value, schema, attribute }, { set }) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isArray, isNil, toPath } from 'lodash/fp';
|
||||
import { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
export default (allowedFields: string[] | null = null): Visitor =>
|
||||
({ key, path: { attribute: path } }, { remove }) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isDynamicZoneAttribute } from '../../content-types';
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
const visitor: Visitor = ({ key, attribute }, { remove }) => {
|
||||
if (isDynamicZoneAttribute(attribute)) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isMorphToRelationalAttribute } from '../../content-types';
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
const visitor: Visitor = ({ key, attribute }, { remove }) => {
|
||||
if (isMorphToRelationalAttribute(attribute)) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
const visitor: Visitor = ({ key, attribute }, { remove }) => {
|
||||
if (attribute?.type === 'password') {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isPrivateAttribute } from '../../content-types';
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
const visitor: Visitor = ({ schema, key, attribute }, { remove }) => {
|
||||
if (!attribute) {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import * as contentTypeUtils from '../../content-types';
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
const ACTIONS_TO_VERIFY = ['find'];
|
||||
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = contentTypeUtils.constants;
|
||||
|
||||
type MorphArray = Array<{ __type: string }>;
|
||||
|
||||
export default (auth: unknown): Visitor =>
|
||||
async ({ data, key, attribute, schema }, { remove, set }) => {
|
||||
if (!attribute) {
|
||||
@ -19,7 +21,7 @@ export default (auth: unknown): Visitor =>
|
||||
const handleMorphRelation = async () => {
|
||||
const newMorphValue: Record<string, unknown>[] = [];
|
||||
|
||||
for (const element of data[key]) {
|
||||
for (const element of (data as Record<string, MorphArray>)[key]) {
|
||||
const scopes = ACTIONS_TO_VERIFY.map((action) => `${element.__type}.${action}`);
|
||||
const isAllowed = await hasAccessToSomeScopes(scopes, auth);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isArray } from 'lodash/fp';
|
||||
import type { Visitor } from '../../traverse-entity';
|
||||
import type { Visitor } from '../../traverse/factory';
|
||||
|
||||
export default (restrictedFields: string[] | null = null): Visitor =>
|
||||
({ key, path: { attribute: path } }, { remove }) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { clone, isObject, isArray, isNil, curry } from 'lodash/fp';
|
||||
import { Attribute, Model } from './types';
|
||||
import { isRelationalAttribute, isMediaAttribute } from './content-types';
|
||||
import { isRelationalAttribute, isMediaAttribute, isComponentAttribute } from './content-types';
|
||||
|
||||
export type VisitorUtils = ReturnType<typeof createVisitorUtils>;
|
||||
|
||||
@ -154,7 +154,7 @@ const traverseEntity = async (visitor: Visitor, options: TraverseOptions, entity
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.type === 'component') {
|
||||
if (isComponentAttribute(attribute)) {
|
||||
const targetSchema = strapi.getModel(attribute.component);
|
||||
|
||||
if (isArray(value)) {
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||
import { isNil, pick } from 'lodash/fp';
|
||||
import { Attribute, Model, RelationalAttribute } from '../types';
|
||||
import {
|
||||
Attribute,
|
||||
ComponentAttribute,
|
||||
DynamicZoneAttribute,
|
||||
Model,
|
||||
RelationalAttribute,
|
||||
} from '../types';
|
||||
|
||||
export interface Path {
|
||||
raw: string | null;
|
||||
@ -89,6 +95,8 @@ interface State {
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_PATH = { raw: null, attribute: null };
|
||||
|
||||
export default () => {
|
||||
const state: State = {
|
||||
parsers: [],
|
||||
@ -100,8 +108,8 @@ export default () => {
|
||||
},
|
||||
};
|
||||
|
||||
const traverse: Traverse = async (visitor, options: TraverseOptions, data) => {
|
||||
const { path = { raw: null, attribute: null }, schema } = options ?? {};
|
||||
const traverse: Traverse = async (visitor, options, data) => {
|
||||
const { path = DEFAULT_PATH, schema } = options ?? {};
|
||||
|
||||
// interceptors
|
||||
for (const { predicate, handler } of state.interceptors) {
|
||||
@ -233,11 +241,11 @@ export default () => {
|
||||
return this.onAttribute(({ attribute }) => attribute?.type === 'media', handler);
|
||||
},
|
||||
|
||||
onComponent(handler: AttributeHandler<RelationalAttribute>['handler']) {
|
||||
onComponent(handler: AttributeHandler<ComponentAttribute>['handler']) {
|
||||
return this.onAttribute(({ attribute }) => attribute?.type === 'component', handler);
|
||||
},
|
||||
|
||||
onDynamicZone(handler: AttributeHandler<RelationalAttribute>['handler']) {
|
||||
onDynamicZone(handler: AttributeHandler<DynamicZoneAttribute>['handler']) {
|
||||
return this.onAttribute(({ attribute }) => attribute?.type === 'dynamiczone', handler);
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { curry, isObject, isEmpty, isArray, isNil, cloneDeep, omit, prop } from 'lodash/fp';
|
||||
import { curry, isObject, isEmpty, isArray, isNil, cloneDeep, omit } from 'lodash/fp';
|
||||
|
||||
import traverseFactory from './factory';
|
||||
|
||||
import { VisitorOptions } from '../../traverse-entity';
|
||||
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
|
||||
|
||||
const filters = traverseFactory()
|
||||
.intercept(
|
||||
@ -13,7 +13,9 @@ const filters = traverseFactory()
|
||||
filters.map((filter, i) => {
|
||||
// In filters, only operators such as $and, $in, $notIn or $or and implicit operators like [...]
|
||||
// can have a value array, thus we can update the raw path but not the attribute one
|
||||
const newPath = { ...options.path, raw: `${options.path.raw}[${i}]` };
|
||||
const newPath = options.path
|
||||
? { ...options.path, raw: `${options.path.raw}[${i}]` }
|
||||
: options.path;
|
||||
|
||||
return recurse(visitor, { ...options, path: newPath }, filter);
|
||||
})
|
||||
@ -23,34 +25,31 @@ const filters = traverseFactory()
|
||||
)
|
||||
.intercept(
|
||||
// Ignore non object filters and return the value as-is
|
||||
(filters) => !isObject(filters),
|
||||
(filters): filters is unknown => !isObject(filters),
|
||||
(_, __, filters) => {
|
||||
return filters;
|
||||
}
|
||||
)
|
||||
// Parse object values
|
||||
.parse(
|
||||
(value) => typeof value === 'object',
|
||||
() => ({
|
||||
transform: cloneDeep,
|
||||
.parse(isObj, () => ({
|
||||
transform: cloneDeep,
|
||||
|
||||
remove(key, data) {
|
||||
return omit(key, data);
|
||||
},
|
||||
remove(key, data) {
|
||||
return omit(key, data);
|
||||
},
|
||||
|
||||
set(key, value, data) {
|
||||
return { ...data, [key]: value };
|
||||
},
|
||||
set(key, value, data) {
|
||||
return { ...data, [key]: value };
|
||||
},
|
||||
|
||||
keys(data) {
|
||||
return Object.keys(data);
|
||||
},
|
||||
keys(data) {
|
||||
return Object.keys(data);
|
||||
},
|
||||
|
||||
get(key, data) {
|
||||
return prop(key, data);
|
||||
},
|
||||
})
|
||||
)
|
||||
get(key, data) {
|
||||
return data[key];
|
||||
},
|
||||
}))
|
||||
// Ignore null or undefined values
|
||||
.ignore(({ value }) => isNil(value))
|
||||
// Recursion on operators (non attributes)
|
||||
|
||||
@ -11,10 +11,12 @@ import {
|
||||
cloneDeep,
|
||||
join,
|
||||
first,
|
||||
omit,
|
||||
} from 'lodash/fp';
|
||||
|
||||
import traverseFactory from './factory';
|
||||
import { Attribute } from '../types';
|
||||
import { isMorphToRelationalAttribute } from '../content-types';
|
||||
|
||||
const isKeyword = (keyword: string) => {
|
||||
return ({ key, attribute }: { key: string; attribute: Attribute }) => {
|
||||
@ -25,6 +27,8 @@ const isKeyword = (keyword: string) => {
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
isArray(value) && value.every(isString);
|
||||
|
||||
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
|
||||
|
||||
const populate = traverseFactory()
|
||||
// Array of strings ['foo', 'foo.bar'] => map(recurse), then filter out empty items
|
||||
.intercept(isStringArray, async (visitor, options, populate, { recurse }) => {
|
||||
@ -57,7 +61,8 @@ const populate = traverseFactory()
|
||||
},
|
||||
|
||||
keys(data) {
|
||||
return [first(tokenize(data))];
|
||||
const v = first(tokenize(data));
|
||||
return v ? [v] : [];
|
||||
},
|
||||
|
||||
get(key, data) {
|
||||
@ -68,7 +73,7 @@ const populate = traverseFactory()
|
||||
};
|
||||
})
|
||||
// Parse object values
|
||||
.parse(isObject, () => ({
|
||||
.parse(isObj, () => ({
|
||||
transform: cloneDeep,
|
||||
|
||||
remove(key, data) {
|
||||
@ -102,7 +107,11 @@ const populate = traverseFactory()
|
||||
}
|
||||
)
|
||||
.on(isKeyword('on'), async ({ key, visitor, path, value }, { set, recurse }) => {
|
||||
const newOn = {};
|
||||
const newOn: Record<string, unknown> = {};
|
||||
|
||||
if (!isObj(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [uid, subPopulate] of Object.entries(value)) {
|
||||
const model = strapi.getModel(uid);
|
||||
@ -117,16 +126,14 @@ const populate = traverseFactory()
|
||||
})
|
||||
// Handle populate on relation
|
||||
.onRelation(async ({ key, value, attribute, visitor, path, schema }, { set, recurse }) => {
|
||||
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
||||
|
||||
if (isMorphRelation) {
|
||||
if (isMorphToRelationalAttribute(attribute)) {
|
||||
// Don't traverse values that cannot be parsed
|
||||
if (!isObject(value) || !isObject(value?.on)) {
|
||||
if (!isObject(value) || !('on' in value && isObject(value?.on))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a populate fragment defined, traverse it
|
||||
const newValue = await recurse(visitor, { schema, path }, { on: value.on });
|
||||
const newValue = await recurse(visitor, { schema, path }, { on: value?.on });
|
||||
|
||||
set(key, { on: newValue });
|
||||
}
|
||||
@ -159,12 +166,11 @@ const populate = traverseFactory()
|
||||
.onDynamicZone(async ({ key, value, attribute, schema, visitor, path }, { set, recurse }) => {
|
||||
if (isObject(value)) {
|
||||
const { components } = attribute;
|
||||
const { on, ...properties } = value;
|
||||
|
||||
const newValue = {};
|
||||
|
||||
// Handle legacy DZ params
|
||||
let newProperties = properties;
|
||||
let newProperties: unknown = omit('on', value);
|
||||
|
||||
for (const componentUID of components) {
|
||||
const componentSchema = strapi.getModel(componentUID);
|
||||
@ -174,8 +180,8 @@ const populate = traverseFactory()
|
||||
Object.assign(newValue, newProperties);
|
||||
|
||||
// Handle new morph fragment syntax
|
||||
if (on) {
|
||||
const newOn = await recurse(visitor, { schema, path }, { on });
|
||||
if ('on' in value && value.on) {
|
||||
const newOn = await recurse(visitor, { schema, path }, { on: value.on });
|
||||
|
||||
// Recompose both syntaxes
|
||||
Object.assign(newValue, newOn);
|
||||
|
||||
@ -26,6 +26,8 @@ const isObjectArray = (value: unknown): value is object[] =>
|
||||
const isNestedSorts = (value: unknown): value is string =>
|
||||
isString(value) && value.split(',').length > 1;
|
||||
|
||||
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
|
||||
|
||||
const sort = traverseFactory()
|
||||
.intercept(
|
||||
// String with chained sorts (foo,bar,foobar) => split, map(recurse), then recompose
|
||||
@ -98,7 +100,8 @@ const sort = traverseFactory()
|
||||
},
|
||||
|
||||
keys(data) {
|
||||
return [first(tokenize(data))];
|
||||
const v = first(tokenize(data));
|
||||
return v ? [v] : [];
|
||||
},
|
||||
|
||||
get(key, data) {
|
||||
@ -109,7 +112,7 @@ const sort = traverseFactory()
|
||||
};
|
||||
})
|
||||
// Parse object values
|
||||
.parse(isObject, () => ({
|
||||
.parse(isObj, () => ({
|
||||
transform: cloneDeep,
|
||||
|
||||
remove(key, data) {
|
||||
|
||||
@ -14,6 +14,14 @@ export interface Attribute {
|
||||
|
||||
export interface RelationalAttribute extends Attribute {
|
||||
relation: string;
|
||||
target: string;
|
||||
}
|
||||
export interface ComponentAttribute extends Attribute {
|
||||
component: string;
|
||||
repeatable?: boolean;
|
||||
}
|
||||
export interface DynamicZoneAttribute extends Attribute {
|
||||
components: string[];
|
||||
}
|
||||
|
||||
export type Kind = 'singleType' | 'collectionType';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import * as yup from 'yup';
|
||||
import _ from 'lodash';
|
||||
import { defaults } from 'lodash/fp';
|
||||
import { defaults, isNumber, isInteger } from 'lodash/fp';
|
||||
import * as utils from './string-formatting';
|
||||
import { YupValidationError } from './errors';
|
||||
import printValue from './print-value';
|
||||
@ -65,46 +65,64 @@ class StrapiIDSchema extends MixedSchemaType {
|
||||
super({ type: 'strapiID' });
|
||||
}
|
||||
|
||||
_typeCheck(value) {
|
||||
return typeof value === 'string' || (Number.isInteger(value) && value >= 0);
|
||||
_typeCheck(value: unknown): value is string | number {
|
||||
return typeof value === 'string' || (isNumber(value) && isInteger(value) && value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
yup.strapiID = () => new StrapiIDSchema();
|
||||
|
||||
const handleYupError = (error, errorMessage) => {
|
||||
const handleYupError = (error: yup.ValidationError, errorMessage: string) => {
|
||||
throw new YupValidationError(error, errorMessage);
|
||||
};
|
||||
|
||||
const defaultValidationParam = { strict: true, abortEarly: false };
|
||||
|
||||
const validateYupSchema =
|
||||
(schema, options = {}) =>
|
||||
async (body, errorMessage) => {
|
||||
(schema: yup.AnySchema, options = {}) =>
|
||||
async (body: unknown, errorMessage: string) => {
|
||||
try {
|
||||
const optionsWithDefaults = defaults(defaultValidationParam, options);
|
||||
const result = await schema.validate(body, optionsWithDefaults);
|
||||
return result;
|
||||
} catch (e) {
|
||||
handleYupError(e, errorMessage);
|
||||
if (e instanceof yup.ValidationError) {
|
||||
handleYupError(e, errorMessage);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const validateYupSchemaSync =
|
||||
(schema, options = {}) =>
|
||||
(body, errorMessage) => {
|
||||
(schema: yup.AnySchema, options = {}) =>
|
||||
(body: unknown, errorMessage: string) => {
|
||||
try {
|
||||
const optionsWithDefaults = defaults(defaultValidationParam, options);
|
||||
return schema.validateSync(body, optionsWithDefaults);
|
||||
} catch (e) {
|
||||
handleYupError(e, errorMessage);
|
||||
if (e instanceof yup.ValidationError) {
|
||||
handleYupError(e, errorMessage);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
interface NoTypeOptions {
|
||||
path: string;
|
||||
type: string;
|
||||
value: unknown;
|
||||
originalValue: unknown;
|
||||
}
|
||||
|
||||
// Temporary fix of this issue : https://github.com/jquense/yup/issues/616
|
||||
yup.setLocale({
|
||||
mixed: {
|
||||
notType({ path, type, value, originalValue }) {
|
||||
notType(options: NoTypeOptions) {
|
||||
const { path, type, value, originalValue } = options;
|
||||
const isCast = originalValue != null && originalValue !== value;
|
||||
const msg =
|
||||
`${path} must be a \`${type}\` type, ` +
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user