mirror of
https://github.com/strapi/strapi.git
synced 2025-09-02 13:23:12 +00:00
feat!: document service query param validation (#21034)
This commit is contained in:
parent
1204e14d29
commit
fbbccd06ea
@ -1,7 +1,8 @@
|
|||||||
import { omit, assoc, merge, curry } from 'lodash/fp';
|
import { omit, assoc, merge, curry } from 'lodash/fp';
|
||||||
|
|
||||||
import { async, contentTypes as contentTypesUtils } from '@strapi/utils';
|
import { async, contentTypes as contentTypesUtils, validate } from '@strapi/utils';
|
||||||
|
|
||||||
|
import { UID } from '@strapi/types';
|
||||||
import { wrapInTransaction, type RepositoryFactoryMethod } from './common';
|
import { wrapInTransaction, type RepositoryFactoryMethod } from './common';
|
||||||
import * as DP from './draft-and-publish';
|
import * as DP from './draft-and-publish';
|
||||||
import * as i18n from './internationalization';
|
import * as i18n from './internationalization';
|
||||||
@ -15,10 +16,38 @@ import { transformParamsToQuery } from './transform/query';
|
|||||||
import { transformParamsDocumentId } from './transform/id-transform';
|
import { transformParamsDocumentId } from './transform/id-transform';
|
||||||
import { createEventManager } from './events';
|
import { createEventManager } from './events';
|
||||||
|
|
||||||
|
const { validators } = validate;
|
||||||
|
|
||||||
|
// we have to typecast to reconcile the differences between validator and database getModel
|
||||||
|
const getModel = ((schema: UID.Schema) => strapi.getModel(schema)) as (schema: string) => any;
|
||||||
|
|
||||||
export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
||||||
const contentType = strapi.contentType(uid);
|
const contentType = strapi.contentType(uid);
|
||||||
const hasDraftAndPublish = contentTypesUtils.hasDraftAndPublish(contentType);
|
const hasDraftAndPublish = contentTypesUtils.hasDraftAndPublish(contentType);
|
||||||
|
|
||||||
|
// Define the validations that should be performed
|
||||||
|
const sortValidations = ['nonAttributesOperators', 'dynamicZones', 'morphRelations'];
|
||||||
|
const fieldValidations = ['scalarAttributes'];
|
||||||
|
const filtersValidations = ['nonAttributesOperators', 'dynamicZones', 'morphRelations'];
|
||||||
|
const populateValidations = {
|
||||||
|
sort: sortValidations,
|
||||||
|
field: fieldValidations,
|
||||||
|
filters: filtersValidations,
|
||||||
|
populate: ['nonAttributesOperators'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateParams = async (params: any) => {
|
||||||
|
const ctx = { schema: contentType, getModel };
|
||||||
|
await validators.validateFilters(ctx, params.filters, filtersValidations);
|
||||||
|
await validators.validateSort(ctx, params.sort, sortValidations);
|
||||||
|
await validators.validateFields(ctx, params.fields, fieldValidations);
|
||||||
|
await validators.validatePopulate(ctx, params.populate, populateValidations);
|
||||||
|
|
||||||
|
// TODO: add validate status, locale, pagination
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
const entries = createEntriesService(uid);
|
const entries = createEntriesService(uid);
|
||||||
|
|
||||||
const eventManager = createEventManager(strapi, uid);
|
const eventManager = createEventManager(strapi, uid);
|
||||||
@ -26,6 +55,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
|
|
||||||
async function findMany(params = {} as any) {
|
async function findMany(params = {} as any) {
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.defaultToDraft,
|
DP.defaultToDraft,
|
||||||
DP.statusToLookup(contentType),
|
DP.statusToLookup(contentType),
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
@ -39,6 +69,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
|
|
||||||
async function findFirst(params = {} as any) {
|
async function findFirst(params = {} as any) {
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.defaultToDraft,
|
DP.defaultToDraft,
|
||||||
DP.statusToLookup(contentType),
|
DP.statusToLookup(contentType),
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
@ -55,6 +86,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.defaultToDraft,
|
DP.defaultToDraft,
|
||||||
DP.statusToLookup(contentType),
|
DP.statusToLookup(contentType),
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
@ -71,6 +103,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
omit('status'),
|
omit('status'),
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
i18n.multiLocaleToLookup(contentType),
|
i18n.multiLocaleToLookup(contentType),
|
||||||
@ -98,6 +131,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const queryParams = await async.pipe(
|
const queryParams = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.filterDataPublishedAt,
|
DP.filterDataPublishedAt,
|
||||||
DP.setStatusToDraft(contentType),
|
DP.setStatusToDraft(contentType),
|
||||||
DP.statusToData(contentType),
|
DP.statusToData(contentType),
|
||||||
@ -123,6 +157,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const queryParams = await async.pipe(
|
const queryParams = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.filterDataPublishedAt,
|
DP.filterDataPublishedAt,
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
i18n.multiLocaleToLookup(contentType)
|
i18n.multiLocaleToLookup(contentType)
|
||||||
@ -143,6 +178,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const clonedEntries = await async.map(
|
const clonedEntries = await async.map(
|
||||||
entriesToClone,
|
entriesToClone,
|
||||||
async.pipe(
|
async.pipe(
|
||||||
|
validateParams,
|
||||||
omit('id'),
|
omit('id'),
|
||||||
// assign new documentId
|
// assign new documentId
|
||||||
assoc('documentId', createDocumentId()),
|
assoc('documentId', createDocumentId()),
|
||||||
@ -161,6 +197,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const queryParams = await async.pipe(
|
const queryParams = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.filterDataPublishedAt,
|
DP.filterDataPublishedAt,
|
||||||
DP.setStatusToDraft(contentType),
|
DP.setStatusToDraft(contentType),
|
||||||
DP.statusToLookup(contentType),
|
DP.statusToLookup(contentType),
|
||||||
@ -212,6 +249,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
|
|
||||||
async function count(params = {} as any) {
|
async function count(params = {} as any) {
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
DP.defaultStatus(contentType),
|
DP.defaultStatus(contentType),
|
||||||
DP.statusToLookup(contentType),
|
DP.statusToLookup(contentType),
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
@ -226,6 +264,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const queryParams = await async.pipe(
|
const queryParams = await async.pipe(
|
||||||
|
validateParams,
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
i18n.multiLocaleToLookup(contentType)
|
i18n.multiLocaleToLookup(contentType)
|
||||||
)(params);
|
)(params);
|
||||||
@ -266,6 +305,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const query = await async.pipe(
|
const query = await async.pipe(
|
||||||
|
validateParams,
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
i18n.multiLocaleToLookup(contentType),
|
i18n.multiLocaleToLookup(contentType),
|
||||||
transformParamsToQuery(uid),
|
transformParamsToQuery(uid),
|
||||||
@ -284,6 +324,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
|
|||||||
const { documentId, ...params } = opts;
|
const { documentId, ...params } = opts;
|
||||||
|
|
||||||
const queryParams = await async.pipe(
|
const queryParams = await async.pipe(
|
||||||
|
validateParams,
|
||||||
i18n.defaultLocale(contentType),
|
i18n.defaultLocale(contentType),
|
||||||
i18n.multiLocaleToLookup(contentType)
|
i18n.multiLocaleToLookup(contentType)
|
||||||
)(params);
|
)(params);
|
||||||
|
@ -79,7 +79,7 @@ interface CommonHandler<AttributeType = Attribute> {
|
|||||||
|
|
||||||
export interface TransformUtils {
|
export interface TransformUtils {
|
||||||
remove(key: string): void;
|
remove(key: string): void;
|
||||||
set(key: string, valeu: unknown): void;
|
set(key: string, value: unknown): void;
|
||||||
recurse: Traverse;
|
recurse: Traverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,24 @@ import { curry, isArray, isString, eq, trim, constant } from 'lodash/fp';
|
|||||||
|
|
||||||
import traverseFactory from './factory';
|
import traverseFactory from './factory';
|
||||||
|
|
||||||
const isStringArray = (value: unknown): value is string[] =>
|
const isStringArray = (value: unknown): value is string[] => {
|
||||||
isArray(value) && value.every(isString);
|
return isArray(value) && value.every(isString);
|
||||||
|
};
|
||||||
|
|
||||||
const fields = traverseFactory()
|
const fields = traverseFactory()
|
||||||
// Intercept array of strings
|
// Intercept array of strings
|
||||||
|
// e.g. fields=['title', 'description']
|
||||||
.intercept(isStringArray, async (visitor, options, fields, { recurse }) => {
|
.intercept(isStringArray, async (visitor, options, fields, { recurse }) => {
|
||||||
return Promise.all(fields.map((field) => recurse(visitor, options, field)));
|
return Promise.all(fields.map((field) => recurse(visitor, options, field)));
|
||||||
})
|
})
|
||||||
|
// Intercept comma separated fields (as string)
|
||||||
|
// e.g. fields='title,description'
|
||||||
|
.intercept(
|
||||||
|
(value): value is string => isString(value) && value.includes(','),
|
||||||
|
(visitor, options, fields, { recurse }) => {
|
||||||
|
return Promise.all(fields.split(',').map((field) => recurse(visitor, options, field)));
|
||||||
|
}
|
||||||
|
)
|
||||||
// Return wildcards as is
|
// Return wildcards as is
|
||||||
.intercept((value): value is string => eq('*', value), constant('*'))
|
.intercept((value): value is string => eq('*', value), constant('*'))
|
||||||
// Parse string values
|
// Parse string values
|
||||||
|
@ -132,6 +132,8 @@ const populate = traverseFactory()
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.ignore(({ key, attribute }) => {
|
.ignore(({ key, attribute }) => {
|
||||||
|
// we don't want to recurse using traversePopulate and instead let
|
||||||
|
// the visitors recurse with the appropriate traversal (sort, filters, etc...)
|
||||||
return ['sort', 'filters', 'fields'].includes(key) && !attribute;
|
return ['sort', 'filters', 'fields'].includes(key) && !attribute;
|
||||||
})
|
})
|
||||||
.on(
|
.on(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ValidationError } from '../errors';
|
import { ValidationError } from '../errors';
|
||||||
|
|
||||||
export const throwInvalidKey = ({ key, path }: { key: string; path?: string | null }) => {
|
export const throwInvalidKey = ({ key, path }: { key: string; path?: string | null }): never => {
|
||||||
const msg = path && path !== key ? `Invalid key ${key} at ${path}` : `Invalid key ${key}`;
|
const msg = path && path !== key ? `Invalid key ${key} at ${path}` : `Invalid key ${key}`;
|
||||||
|
|
||||||
throw new ValidationError(msg, {
|
throw new ValidationError(msg, {
|
||||||
@ -8,3 +8,17 @@ export const throwInvalidKey = ({ key, path }: { key: string; path?: string | nu
|
|||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// lodash/fp curry does not detect async methods, so we'll use our own that is typed correctly
|
||||||
|
export const asyncCurry = <A extends unknown[], R>(
|
||||||
|
fn: (...args: A) => Promise<R>
|
||||||
|
): ((...args: Partial<A>) => any) => {
|
||||||
|
const curried = (...args: unknown[]): unknown => {
|
||||||
|
if (args.length >= fn.length) {
|
||||||
|
return fn(...(args as A));
|
||||||
|
}
|
||||||
|
return (...moreArgs: unknown[]) => curried(...args, ...moreArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
return curried;
|
||||||
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { curry, isEmpty, isNil } from 'lodash/fp';
|
import { isEmpty, isNil, isObject } from 'lodash/fp';
|
||||||
|
|
||||||
import { pipe as pipeAsync } from '../async';
|
import { pipe as pipeAsync } from '../async';
|
||||||
import traverseEntity from '../traverse-entity';
|
import traverseEntity from '../traverse-entity';
|
||||||
@ -11,8 +11,9 @@ import {
|
|||||||
} from '../traverse';
|
} from '../traverse';
|
||||||
import { throwPassword, throwPrivate, throwDynamicZones, throwMorphToRelations } from './visitors';
|
import { throwPassword, throwPrivate, throwDynamicZones, throwMorphToRelations } from './visitors';
|
||||||
import { isOperator } from '../operators';
|
import { isOperator } from '../operators';
|
||||||
import { throwInvalidKey } from './utils';
|
import { asyncCurry, throwInvalidKey } from './utils';
|
||||||
import type { Model, Data } from '../types';
|
import type { Model, Data } from '../types';
|
||||||
|
import parseType from '../parse-type';
|
||||||
|
|
||||||
const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = constants;
|
const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = constants;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ interface Context {
|
|||||||
getModel: (model: string) => Model;
|
getModel: (model: string) => Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throwPasswords = (ctx: Context) => async (entity: Data) => {
|
export const throwPasswords = (ctx: Context) => async (entity: Data) => {
|
||||||
if (!ctx.schema) {
|
if (!ctx.schema) {
|
||||||
throw new Error('Missing schema in throwPasswords');
|
throw new Error('Missing schema in throwPasswords');
|
||||||
}
|
}
|
||||||
@ -29,176 +30,386 @@ const throwPasswords = (ctx: Context) => async (entity: Data) => {
|
|||||||
return traverseEntity(throwPassword, ctx, entity);
|
return traverseEntity(throwPassword, ctx, entity);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValidateFilters = curry((ctx: Context, filters: unknown) => {
|
type AnyFunc = (...args: any[]) => any;
|
||||||
// TODO: schema checks should check that it is a validate schema with yup
|
|
||||||
if (!ctx.schema) {
|
export const FILTER_TRAVERSALS = [
|
||||||
throw new Error('Missing schema in defaultValidateFilters');
|
'nonAttributesOperators',
|
||||||
}
|
'dynamicZones',
|
||||||
|
'morphRelations',
|
||||||
|
'passwords',
|
||||||
|
'private',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateFilters = asyncCurry(
|
||||||
|
async (ctx: Context, filters: unknown, include: (typeof FILTER_TRAVERSALS)[number][]) => {
|
||||||
|
// TODO: schema checks should check that it is a valid schema with yup
|
||||||
|
if (!ctx.schema) {
|
||||||
|
throw new Error('Missing schema in defaultValidateFilters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of functions conditionally
|
||||||
|
const functionsToApply: Array<AnyFunc> = [];
|
||||||
|
|
||||||
return pipeAsync(
|
|
||||||
// keys that are not attributes or valid operators
|
// keys that are not attributes or valid operators
|
||||||
traverseQueryFilters(({ key, attribute, path }) => {
|
if (include.includes('nonAttributesOperators')) {
|
||||||
// ID is not an attribute per se, so we need to make
|
functionsToApply.push(
|
||||||
// an extra check to ensure we're not removing it
|
traverseQueryFilters(({ key, attribute, path }) => {
|
||||||
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
// ID is not an attribute per se, so we need to make
|
||||||
return;
|
// an extra check to ensure we're not removing it
|
||||||
}
|
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isAttribute = !!attribute;
|
const isAttribute = !!attribute;
|
||||||
|
|
||||||
if (!isAttribute && !isOperator(key)) {
|
if (!isAttribute && !isOperator(key)) {
|
||||||
throwInvalidKey({ key, path: path.attribute });
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
}
|
}
|
||||||
}, ctx),
|
}, ctx)
|
||||||
// dynamic zones from filters
|
);
|
||||||
traverseQueryFilters(throwDynamicZones, ctx),
|
}
|
||||||
// morphTo relations from filters; because you can't have deep filtering on morph relations
|
|
||||||
traverseQueryFilters(throwMorphToRelations, ctx),
|
if (include.includes('dynamicZones')) {
|
||||||
// passwords from filters
|
functionsToApply.push(traverseQueryFilters(throwDynamicZones, ctx));
|
||||||
traverseQueryFilters(throwPassword, ctx),
|
}
|
||||||
// private from filters
|
|
||||||
traverseQueryFilters(throwPrivate, ctx)
|
if (include.includes('morphRelations')) {
|
||||||
// we allow empty objects to validate and only sanitize them out, so that users may write "lazy" queries without checking their params exist
|
functionsToApply.push(traverseQueryFilters(throwMorphToRelations, ctx));
|
||||||
)(filters);
|
}
|
||||||
|
|
||||||
|
if (include.includes('passwords')) {
|
||||||
|
functionsToApply.push(traverseQueryFilters(throwPassword, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include.includes('private')) {
|
||||||
|
functionsToApply.push(traverseQueryFilters(throwPrivate, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return directly if no validation functions are provided
|
||||||
|
if (functionsToApply.length === 0) {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeAsync(...functionsToApply)(filters);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const defaultValidateFilters = asyncCurry(async (ctx: Context, filters: unknown) => {
|
||||||
|
return validateFilters(ctx, filters, FILTER_TRAVERSALS);
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValidateSort = curry((ctx: Context, sort: unknown) => {
|
export const SORT_TRAVERSALS = [
|
||||||
if (!ctx.schema) {
|
'nonAttributesOperators',
|
||||||
throw new Error('Missing schema in defaultValidateSort');
|
'dynamicZones',
|
||||||
|
'morphRelations',
|
||||||
|
'passwords',
|
||||||
|
'private',
|
||||||
|
'nonScalarEmptyKeys',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateSort = asyncCurry(
|
||||||
|
async (ctx: Context, sort: unknown, include: (typeof SORT_TRAVERSALS)[number][]) => {
|
||||||
|
if (!ctx.schema) {
|
||||||
|
throw new Error('Missing schema in defaultValidateSort');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of functions conditionally based on the include array
|
||||||
|
const functionsToApply: Array<AnyFunc> = [];
|
||||||
|
|
||||||
|
// Validate non attribute keys
|
||||||
|
if (include.includes('nonAttributesOperators')) {
|
||||||
|
functionsToApply.push(
|
||||||
|
traverseQuerySort(({ key, attribute, path }) => {
|
||||||
|
// ID is not an attribute per se, so we need to make
|
||||||
|
// an extra check to ensure we're not removing it
|
||||||
|
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attribute) {
|
||||||
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
|
}
|
||||||
|
}, ctx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dynamic zones from sort
|
||||||
|
if (include.includes('dynamicZones')) {
|
||||||
|
functionsToApply.push(traverseQuerySort(throwDynamicZones, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate morphTo relations from sort
|
||||||
|
if (include.includes('morphRelations')) {
|
||||||
|
functionsToApply.push(traverseQuerySort(throwMorphToRelations, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate passwords from sort
|
||||||
|
if (include.includes('passwords')) {
|
||||||
|
functionsToApply.push(traverseQuerySort(throwPassword, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate private from sort
|
||||||
|
if (include.includes('private')) {
|
||||||
|
functionsToApply.push(traverseQuerySort(throwPrivate, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate non-scalar empty keys
|
||||||
|
if (include.includes('nonScalarEmptyKeys')) {
|
||||||
|
functionsToApply.push(
|
||||||
|
traverseQuerySort(({ key, attribute, value, path }) => {
|
||||||
|
// ID is not an attribute per se, so we need to make
|
||||||
|
// an extra check to ensure we're not removing it
|
||||||
|
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isScalarAttribute(attribute) && isEmpty(value)) {
|
||||||
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
|
}
|
||||||
|
}, ctx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return directly if no validation functions are provided
|
||||||
|
if (functionsToApply.length === 0) {
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeAsync(...functionsToApply)(sort);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return pipeAsync(
|
export const defaultValidateSort = asyncCurry(async (ctx: Context, sort: unknown) => {
|
||||||
// non attribute keys
|
return validateSort(ctx, sort, SORT_TRAVERSALS);
|
||||||
traverseQuerySort(({ key, attribute, path }) => {
|
|
||||||
// ID is not an attribute per se, so we need to make
|
|
||||||
// an extra check to ensure we're not removing it
|
|
||||||
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attribute) {
|
|
||||||
throwInvalidKey({ key, path: path.attribute });
|
|
||||||
}
|
|
||||||
}, ctx),
|
|
||||||
// dynamic zones from sort
|
|
||||||
traverseQuerySort(throwDynamicZones, ctx),
|
|
||||||
// morphTo relations from sort
|
|
||||||
traverseQuerySort(throwMorphToRelations, ctx),
|
|
||||||
// private from sort
|
|
||||||
traverseQuerySort(throwPrivate, ctx),
|
|
||||||
// passwords from filters
|
|
||||||
traverseQuerySort(throwPassword, ctx),
|
|
||||||
// keys for empty non-scalar values
|
|
||||||
traverseQuerySort(({ key, attribute, value, path }) => {
|
|
||||||
// ID is not an attribute per se, so we need to make
|
|
||||||
// an extra check to ensure we're not removing it
|
|
||||||
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isScalarAttribute(attribute) && isEmpty(value)) {
|
|
||||||
throwInvalidKey({ key, path: path.attribute });
|
|
||||||
}
|
|
||||||
}, ctx)
|
|
||||||
)(sort);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValidateFields = curry((ctx: Context, fields: unknown) => {
|
export const FIELDS_TRAVERSALS = ['scalarAttributes', 'privateFields', 'passwordFields'];
|
||||||
if (!ctx.schema) {
|
|
||||||
throw new Error('Missing schema in defaultValidateFields');
|
export const validateFields = asyncCurry(
|
||||||
}
|
async (ctx: Context, fields: unknown, include: (typeof FIELDS_TRAVERSALS)[number][]) => {
|
||||||
|
if (!ctx.schema) {
|
||||||
|
throw new Error('Missing schema in defaultValidateFields');
|
||||||
|
}
|
||||||
|
// Build the list of functions conditionally based on the include array
|
||||||
|
const functionsToApply: Array<AnyFunc> = [];
|
||||||
|
|
||||||
return pipeAsync(
|
|
||||||
// Only allow scalar attributes
|
// Only allow scalar attributes
|
||||||
traverseQueryFields(({ key, attribute, path }) => {
|
if (include.includes('scalarAttributes')) {
|
||||||
// ID is not an attribute per se, so we need to make
|
functionsToApply.push(
|
||||||
// an extra check to ensure we're not throwing because of it
|
traverseQueryFields(({ key, attribute, path }) => {
|
||||||
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
// ID is not an attribute per se, so we need to make
|
||||||
return;
|
// an extra check to ensure we're not throwing because of it
|
||||||
}
|
if ([ID_ATTRIBUTE, DOC_ID_ATTRIBUTE].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isNil(attribute) || !isScalarAttribute(attribute)) {
|
if (isNil(attribute) || !isScalarAttribute(attribute)) {
|
||||||
throwInvalidKey({ key, path: path.attribute });
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
}
|
}
|
||||||
}, ctx),
|
}, ctx)
|
||||||
// private fields
|
);
|
||||||
traverseQueryFields(throwPrivate, ctx),
|
}
|
||||||
// password fields
|
|
||||||
traverseQueryFields(throwPassword, ctx)
|
// Private fields
|
||||||
)(fields);
|
if (include.includes('privateFields')) {
|
||||||
|
functionsToApply.push(traverseQueryFields(throwPrivate, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password fields
|
||||||
|
if (include.includes('passwordFields')) {
|
||||||
|
functionsToApply.push(traverseQueryFields(throwPassword, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return directly if no validation functions are provided
|
||||||
|
if (functionsToApply.length === 0) {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeAsync(...functionsToApply)(fields);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const defaultValidateFields = asyncCurry(async (ctx: Context, fields: unknown) => {
|
||||||
|
return validateFields(ctx, fields, FIELDS_TRAVERSALS);
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValidatePopulate = curry((ctx: Context, populate: unknown) => {
|
export const POPULATE_TRAVERSALS = ['nonAttributesOperators', 'private'];
|
||||||
|
|
||||||
|
export const validatePopulate = asyncCurry(
|
||||||
|
async (
|
||||||
|
ctx: Context,
|
||||||
|
populate: unknown,
|
||||||
|
includes: {
|
||||||
|
fields?: (typeof FIELDS_TRAVERSALS)[number][];
|
||||||
|
sort?: (typeof SORT_TRAVERSALS)[number][];
|
||||||
|
filters?: (typeof FILTER_TRAVERSALS)[number][];
|
||||||
|
populate?: (typeof POPULATE_TRAVERSALS)[number][];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (!ctx.schema) {
|
||||||
|
throw new Error('Missing schema in defaultValidatePopulate');
|
||||||
|
}
|
||||||
|
// Build the list of functions conditionally based on the include array
|
||||||
|
const functionsToApply: Array<AnyFunc> = [];
|
||||||
|
|
||||||
|
// Always include the main traversal function
|
||||||
|
functionsToApply.push(
|
||||||
|
traverseQueryPopulate(async ({ key, path, value, schema, attribute, getModel }, { set }) => {
|
||||||
|
if (attribute) {
|
||||||
|
const isPopulatableAttribute = ['relation', 'dynamiczone', 'component', 'media'].includes(
|
||||||
|
attribute.type
|
||||||
|
);
|
||||||
|
|
||||||
|
// Throw on non-populate attributes
|
||||||
|
if (!isPopulatableAttribute) {
|
||||||
|
throwInvalidKey({ key, path: path.raw });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid populatable attribute, so return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're looking at a populate fragment, ensure its target is valid
|
||||||
|
if (key === 'on') {
|
||||||
|
// Populate fragment should always be an object
|
||||||
|
if (!isObject(value)) {
|
||||||
|
return throwInvalidKey({ key, path: path.raw });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = Object.keys(value);
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const model = getModel(target);
|
||||||
|
|
||||||
|
// If a target is invalid (no matching model), then raise an error
|
||||||
|
if (!model) {
|
||||||
|
throwInvalidKey({ key: target, path: `${path.raw}.${target}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the fragment's target is fine, then let it pass
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore plain wildcards
|
||||||
|
if (key === '' && value === '*') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure count is a boolean
|
||||||
|
if (key === 'count') {
|
||||||
|
try {
|
||||||
|
parseType({ type: 'boolean', value });
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed boolean-like keywords should be ignored
|
||||||
|
try {
|
||||||
|
parseType({ type: 'boolean', value: key });
|
||||||
|
// Key is an allowed boolean-like keyword, skipping validation...
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Continue, because it's not a boolean-like
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested `sort` validation with custom or default traversals
|
||||||
|
if (key === 'sort') {
|
||||||
|
set(
|
||||||
|
key,
|
||||||
|
await validateSort(
|
||||||
|
{
|
||||||
|
schema,
|
||||||
|
getModel,
|
||||||
|
},
|
||||||
|
value, // pass the sort value
|
||||||
|
includes?.sort || SORT_TRAVERSALS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested `filters` validation with custom or default traversals
|
||||||
|
if (key === 'filters') {
|
||||||
|
set(
|
||||||
|
key,
|
||||||
|
await validateFilters(
|
||||||
|
{
|
||||||
|
schema,
|
||||||
|
getModel,
|
||||||
|
},
|
||||||
|
value, // pass the filters value
|
||||||
|
includes?.filters || FILTER_TRAVERSALS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested `fields` validation with custom or default traversals
|
||||||
|
if (key === 'fields') {
|
||||||
|
set(
|
||||||
|
key,
|
||||||
|
await validateFields(
|
||||||
|
{
|
||||||
|
schema,
|
||||||
|
getModel,
|
||||||
|
},
|
||||||
|
value, // pass the fields value
|
||||||
|
includes?.fields || FIELDS_TRAVERSALS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle recursive nested `populate` validation with the same include object
|
||||||
|
if (key === 'populate') {
|
||||||
|
set(
|
||||||
|
key,
|
||||||
|
await validatePopulate(
|
||||||
|
{
|
||||||
|
schema,
|
||||||
|
getModel,
|
||||||
|
},
|
||||||
|
value, // pass the nested populate value
|
||||||
|
includes // pass down the same includes object
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error if non-attribute operators are included in the populate array
|
||||||
|
if (includes?.populate?.includes('nonAttributesOperators')) {
|
||||||
|
throwInvalidKey({ key, path: path.attribute });
|
||||||
|
}
|
||||||
|
}, ctx)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Conditionally traverse for private fields only if 'private' is included
|
||||||
|
if (includes?.populate?.includes('private')) {
|
||||||
|
functionsToApply.push(traverseQueryPopulate(throwPrivate, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return directly if no validation functions are provided
|
||||||
|
if (functionsToApply.length === 0) {
|
||||||
|
return populate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeAsync(...functionsToApply)(populate);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const defaultValidatePopulate = asyncCurry(async (ctx: Context, populate: unknown) => {
|
||||||
if (!ctx.schema) {
|
if (!ctx.schema) {
|
||||||
throw new Error('Missing schema in defaultValidatePopulate');
|
throw new Error('Missing schema in defaultValidatePopulate');
|
||||||
}
|
}
|
||||||
|
|
||||||
return pipeAsync(
|
// Call validatePopulate and include all validations by passing in full traversal arrays
|
||||||
traverseQueryPopulate(async ({ key, value, schema, attribute, getModel }, { set }) => {
|
return validatePopulate(ctx, populate, {
|
||||||
if (attribute) {
|
filters: FILTER_TRAVERSALS,
|
||||||
return;
|
sort: SORT_TRAVERSALS,
|
||||||
}
|
fields: FIELDS_TRAVERSALS,
|
||||||
|
populate: POPULATE_TRAVERSALS,
|
||||||
if (key === 'sort') {
|
});
|
||||||
set(
|
|
||||||
key,
|
|
||||||
await defaultValidateSort(
|
|
||||||
{
|
|
||||||
schema,
|
|
||||||
getModel,
|
|
||||||
},
|
|
||||||
value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'filters') {
|
|
||||||
set(
|
|
||||||
key,
|
|
||||||
await defaultValidateFilters(
|
|
||||||
{
|
|
||||||
schema,
|
|
||||||
getModel,
|
|
||||||
},
|
|
||||||
value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'fields') {
|
|
||||||
set(
|
|
||||||
key,
|
|
||||||
await defaultValidateFields(
|
|
||||||
{
|
|
||||||
schema,
|
|
||||||
getModel,
|
|
||||||
},
|
|
||||||
value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'populate') {
|
|
||||||
set(
|
|
||||||
key,
|
|
||||||
await defaultValidatePopulate(
|
|
||||||
{
|
|
||||||
schema,
|
|
||||||
getModel,
|
|
||||||
},
|
|
||||||
value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, ctx),
|
|
||||||
// Remove private fields
|
|
||||||
traverseQueryPopulate(throwPrivate, ctx)
|
|
||||||
)(populate);
|
|
||||||
});
|
});
|
||||||
export {
|
|
||||||
throwPasswords,
|
|
||||||
defaultValidateFilters,
|
|
||||||
defaultValidateSort,
|
|
||||||
defaultValidateFields,
|
|
||||||
defaultValidatePopulate,
|
|
||||||
};
|
|
||||||
|
@ -551,7 +551,7 @@ const BulkLocaleAction: DocumentActionComponent = ({
|
|||||||
}
|
}
|
||||||
}, [isDraftRelationsError, toggleNotification, formatAPIError]);
|
}, [isDraftRelationsError, toggleNotification, formatAPIError]);
|
||||||
|
|
||||||
if (!schema?.options?.draftAndPublish ?? false) {
|
if (!schema?.options?.draftAndPublish) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@ const registerModelsHooks = () => {
|
|||||||
// Use the id and populate built from non localized fields to get the full
|
// Use the id and populate built from non localized fields to get the full
|
||||||
// result
|
// result
|
||||||
let resultID;
|
let resultID;
|
||||||
if (Array.isArray(result?.entries)) {
|
// TODO: fix bug where an empty array can be returned
|
||||||
|
if (Array.isArray(result?.entries) && result.entries[0]?.id) {
|
||||||
resultID = result.entries[0].id;
|
resultID = result.entries[0].id;
|
||||||
} else if (result?.id) {
|
} else if (result?.id) {
|
||||||
resultID = result.id;
|
resultID = result.id;
|
||||||
|
@ -428,6 +428,17 @@ describe('Core API - Validate', () => {
|
|||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('invalid modifier', () => {
|
||||||
|
it.each([
|
||||||
|
['name_fake (asc)', { name_fake: 'asc' }, defaultDocumentsOrder],
|
||||||
|
['name_fake (desc)', { name_fake: 'desc' }, defaultDocumentsOrder],
|
||||||
|
])('Error with sort: %s', async (_s, sort, order) => {
|
||||||
|
const res = await rq.get('/api/documents', { qs: { sort } });
|
||||||
|
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Password', () => {
|
describe('Password', () => {
|
||||||
@ -444,12 +455,12 @@ describe('Core API - Validate', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Nested sort returns duplicate results. Add back those tests when the issue will be fixed
|
// TODO: Nested sort returns duplicate results. Add back those tests when the issue will be fixed
|
||||||
describe.skip('Relation', () => {
|
describe('Relation', () => {
|
||||||
describe('Scalar', () => {
|
describe('Scalar', () => {
|
||||||
describe('Basic (no modifiers)', () => {
|
describe.skip('Basic (no modifiers)', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['relations.name (asc)', { relations: { name: 'asc' } }, [1, 3, 2]],
|
['relations.name (asc)', { relations: { name: 'asc' } }, [1, 3, 2]],
|
||||||
['relations.sname (desc)', { relations: { name: 'desc' } }, [2, 1, 3]],
|
['relations.name (desc)', { relations: { name: 'desc' } }, [2, 1, 3]],
|
||||||
])('Successfully sort: %s', async (_s, sort, order) => {
|
])('Successfully sort: %s', async (_s, sort, order) => {
|
||||||
const res = await rq.get('/api/documents', { qs: { sort } });
|
const res = await rq.get('/api/documents', { qs: { sort } });
|
||||||
|
|
||||||
@ -538,6 +549,18 @@ describe('Core API - Validate', () => {
|
|||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('invalid modifier', () => {
|
||||||
|
it.each([
|
||||||
|
['name_invalid', 'name_invalid', defaultDocumentsOrder],
|
||||||
|
['name_invalid (asc)', 'name_invalid:asc', defaultDocumentsOrder],
|
||||||
|
['name_invalid (desc)', 'name_invalid:desc', defaultDocumentsOrder],
|
||||||
|
])('Error with sort: %s', async (_s, sort, order) => {
|
||||||
|
const res = await rq.get('/api/documents', { qs: { sort } });
|
||||||
|
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Password', () => {
|
describe('Password', () => {
|
||||||
@ -605,6 +628,17 @@ describe('Core API - Validate', () => {
|
|||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('invalid modifier', () => {
|
||||||
|
it.each([
|
||||||
|
['name_invalid (asc)', [{ name_invalid: 'asc' }], defaultDocumentsOrder],
|
||||||
|
['name_invalid (desc)', [{ name_private: 'desc' }], defaultDocumentsOrder],
|
||||||
|
])('Error on sort: %s', async (_s, sort, order) => {
|
||||||
|
const res = await rq.get('/api/documents', { qs: { sort } });
|
||||||
|
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Password', () => {
|
describe('Password', () => {
|
||||||
|
@ -86,6 +86,12 @@ module.exports = {
|
|||||||
relation: 'manyToMany',
|
relation: 'manyToMany',
|
||||||
target: 'api::category.category',
|
target: 'api::category.category',
|
||||||
},
|
},
|
||||||
|
categories_private: {
|
||||||
|
private: true,
|
||||||
|
type: 'relation',
|
||||||
|
relation: 'manyToMany',
|
||||||
|
target: 'api::category.category',
|
||||||
|
},
|
||||||
dz: {
|
dz: {
|
||||||
pluginOptions: {
|
pluginOptions: {
|
||||||
i18n: {
|
i18n: {
|
||||||
|
@ -0,0 +1,254 @@
|
|||||||
|
import type { Core, Modules } from '@strapi/types';
|
||||||
|
import { errors } from '@strapi/utils';
|
||||||
|
import { createTestSetup, destroyTestSetup } from '../../../../utils/builder-helper';
|
||||||
|
import resources from '../resources/index';
|
||||||
|
import { ARTICLE_UID } from '../utils';
|
||||||
|
|
||||||
|
let strapi: Core.Strapi;
|
||||||
|
|
||||||
|
describe('Document Service Validations', () => {
|
||||||
|
let testUtils;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testUtils = await createTestSetup(resources);
|
||||||
|
strapi = testUtils.strapi;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await destroyTestSetup(testUtils);
|
||||||
|
});
|
||||||
|
|
||||||
|
const methods = [
|
||||||
|
'findMany',
|
||||||
|
'findFirst',
|
||||||
|
'findOne',
|
||||||
|
'publish',
|
||||||
|
'delete',
|
||||||
|
'create',
|
||||||
|
'unpublish',
|
||||||
|
'clone',
|
||||||
|
'update',
|
||||||
|
'discardDraft',
|
||||||
|
'count',
|
||||||
|
];
|
||||||
|
|
||||||
|
describe.each(methods)('%s method', (methodName) => {
|
||||||
|
describe('sort', () => {
|
||||||
|
it('should not throw on existing attribute name', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({ sort: 'title' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on private attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({ sort: 'private' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on password attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({ sort: 'password' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on existing nested (object) key', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: { categories: { sort: { name: 'asc' } } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on existing nested (dot separated) key', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
sort: 'categories.name',
|
||||||
|
populate: 'categories',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ValidationError on invalid key', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({ sort: 'fakekey' })
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filters', () => {
|
||||||
|
it('should not throw on existing attribute equality', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
filters: {
|
||||||
|
title: 'Hello World',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on private attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
filters: {
|
||||||
|
private: 'Hello World',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on password attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
filters: {
|
||||||
|
password: 'Hello World',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on existing nested conditions', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
filters: {
|
||||||
|
title: {
|
||||||
|
$not: {
|
||||||
|
$contains: 'Hello World',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ValidationError on invalid key', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
filters: {
|
||||||
|
fakekey: 'Hello World',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fields', () => {
|
||||||
|
it('should not throw on existing attribute equality', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
fields: ['title'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on private attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
fields: ['private'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on password attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
fields: ['password'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ValidationError on invalid key', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
fields: ['title', 'fakekey'],
|
||||||
|
})
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on valid comma separated keys', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({ fields: 'title,password,private' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on invalid comma separated keys', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({ fields: 'title,invalid' })
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('populate', () => {
|
||||||
|
it('should not throw on populatable attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: ['categories'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on private attribute', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: ['categories_private'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on wildcard *', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: '*',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on dz (boolean)', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: {
|
||||||
|
identifiersDz: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on dz - comp (boolean)', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: {
|
||||||
|
identifiersDz: {
|
||||||
|
on: {
|
||||||
|
'article.compo-unique-all': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on dz', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: {
|
||||||
|
identifiersDz: {
|
||||||
|
on: {
|
||||||
|
'article.compo-unique-all': {
|
||||||
|
fields: ['ComponentTextShort'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on nested wildcard populate', async () => {
|
||||||
|
await strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: {
|
||||||
|
identifiersDz: {
|
||||||
|
on: {
|
||||||
|
'article.compo-unique-all': {
|
||||||
|
populate: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: functionality is not yet implemented
|
||||||
|
it('should throw ValidationError on invalid dz component', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: {
|
||||||
|
identifiersDz: {
|
||||||
|
on: {
|
||||||
|
invalidkey: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ValidationError on non-populatable attribute', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: ['title'],
|
||||||
|
})
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ValidationError on invalid key', async () => {
|
||||||
|
await expect(
|
||||||
|
strapi.documents(ARTICLE_UID)[methodName]({
|
||||||
|
populate: ['categories', 'fakekey'],
|
||||||
|
})
|
||||||
|
).rejects.toThrow(errors.ValidationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user