Update content-types

This commit is contained in:
Alexandre Bodin 2023-06-06 10:58:36 +02:00
parent 62837bf4ce
commit 877ccaa485
31 changed files with 255 additions and 180 deletions

View File

@ -5,6 +5,6 @@ module.exports = {
transform: {
'^.+\\.ts$': ['@swc/jest'],
},
testMatch: ['<rootDir>/**/*.test.js'],
testMatch: ['<rootDir>/**/*.test.(j|t)s'],
displayName: 'Core utils',
};

View File

@ -1,6 +1,4 @@
'use strict';
const { pipeAsync, mapAsync, reduceAsync } = require('../async');
import { pipeAsync, mapAsync, reduceAsync } from '../async';
describe('Async utils', () => {
describe('pipeAsync', () => {

View File

@ -1,6 +1,4 @@
'use strict';
const {
import {
isPrivateAttribute,
isTypedAttribute,
getPrivateAttributes,
@ -8,7 +6,7 @@ const {
getNonWritableAttributes,
getScalarAttributes,
constants,
} = require('../content-types');
} from '../content-types';
const createModelWithPrivates = (privateAttributes = []) => ({
uid: 'myModel',

View File

@ -1,6 +1,4 @@
'use strict';
const envHelper = require('../env-helper');
import envHelper from '../env-helper';
describe('Env helper', () => {
describe('env without cast', () => {

View File

@ -1,6 +1,4 @@
'use strict';
const hooks = require('../hooks');
import * as hooks from '../hooks';
describe('Hooks Module', () => {
describe('Internals', () => {
@ -16,7 +14,7 @@ describe('Hooks Module', () => {
test('Call is not implemented by default', async () => {
const hook = hooks.internals.createHook();
const doCall = () => hook.call('foo');
const doCall = () => hook.call();
expect(doCall).toThrowError('Method not implemented');
});

View File

@ -1,7 +1,5 @@
'use strict';
const path = require('path');
const importDefault = require('../../import-default');
import path from 'node:path';
import importDefault from '../../import-default';
const getPath = (file) => path.resolve(__dirname, file);

View File

@ -1,6 +1,4 @@
'use strict';
const { withDefaultPagination } = require('../pagination');
import { withDefaultPagination } from '../pagination';
const defaultLimit = 20;
const defaults = {

View File

@ -1,7 +1,5 @@
'use strict';
const format = require('date-fns/format');
const parseType = require('../parse-type');
import format from 'date-fns/format';
import parseType from '../parse-type';
describe('parseType', () => {
describe('boolean', () => {

View File

@ -1,6 +1,4 @@
'use strict';
const policyUtils = require('../policy');
import * as policyUtils from '../policy';
describe('Policy util', () => {
describe('Get policy', () => {

View File

@ -1,6 +1,4 @@
'use strict';
const providerFactory = require('../provider-factory');
import providerFactory from '../provider-factory';
const providerMethods = [
'register',
@ -143,7 +141,7 @@ describe('Provider Factory', () => {
expect(provider.get(key)).toBe(item);
await provider.delete(key, item);
await provider.delete(key);
expect(provider.get(key)).toBeUndefined();
});

View File

@ -1,12 +1,18 @@
'use strict';
const { getRelationalFields } = require('../relations');
import { getRelationalFields } from '../relations';
describe('Relations', () => {
describe('getRelationalFields', () => {
test('Attribute must have a type relation', () => {
expect(
getRelationalFields({
kind: 'collectionType',
info: {
singularName: 'test',
pluralName: 'test',
},
options: {
populateCreatorFields: false,
},
attributes: {
rel: {
type: 'relation',

View File

@ -1,6 +1,4 @@
'use strict';
const {
import {
escapeQuery,
stringIncludes,
stringEquals,
@ -8,7 +6,7 @@ const {
getCommonPath,
toRegressedEnumValue,
joinBy,
} = require('../string-formatting');
} from '../string-formatting';
describe('string-formatting', () => {
describe('Escape Query', () => {
@ -140,7 +138,8 @@ describe('string-formatting', () => {
[['/', 'a//', '//b//', '//c'], 'a/b/c'],
[['/', '///a///', '///b///', '///c///'], '///a/b/c///'],
])('%s => %s', (args, expectedResult) => {
expect(joinBy(...args)).toBe(expectedResult);
const [joint, ...rest] = args;
expect(joinBy(joint, ...rest)).toBe(expectedResult);
});
});
});

View File

@ -1,6 +1,4 @@
'use strict';
const { yup } = require('../validators');
import { yup } from '../validators';
describe('validators', () => {
describe('strapiID', () => {

View File

@ -1,8 +1,8 @@
'use strict';
import * as visitors from '../sanitize/visitors';
import * as contentTypeUtils from '../content-types';
import type { Model } from '../types';
const visitors = require('../sanitize/visitors');
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = require('../content-types').constants;
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = contentTypeUtils.constants;
describe('Sanitize visitors util', () => {
describe('removeRestrictedRelations', () => {
@ -25,7 +25,20 @@ describe('Sanitize visitors util', () => {
data,
key,
attribute,
schema: { options: { populateCreatorFields: true } },
schema: {
kind: 'collectionType',
info: {
singularName: 'test',
pluralName: 'tests',
},
options: { populateCreatorFields: true },
attributes: {},
},
value: {},
path: {
attribute: null,
raw: null,
},
},
{ remove, set }
);
@ -45,7 +58,20 @@ describe('Sanitize visitors util', () => {
data,
key,
attribute,
schema: { options: { populateCreatorFields: false } },
schema: {
kind: 'collectionType',
info: {
singularName: 'test',
pluralName: 'tests',
},
options: { populateCreatorFields: false },
attributes: {},
},
value: {},
path: {
attribute: null,
raw: null,
},
},
{ remove, set }
);

View File

@ -1,8 +1,6 @@
'use strict';
const { yup } = require('../validators');
const { formatYupErrors } = require('../format-yup-error');
const { YupValidationError } = require('../errors');
import { yup } from '../validators';
import { formatYupErrors } from '../format-yup-error';
import { YupValidationError } from '../errors';
describe('formatYupErrors', () => {
test('Error message is sanitized', async () => {

View File

@ -1,56 +1,57 @@
import pMap from 'p-map';
import { curry, curryN } from 'lodash/fp';
import type { CurriedFunction3 } from 'lodash';
interface MapOptions {
concurrency?: number;
}
type AnyFunc = (...args: any) => any;
type MapFunc<T = unknown, R = unknown> = (element: T, index: number) => R | Promise<R>;
type PipeArgs<F extends AnyFunc[], PrevReturn = Parameters<F[0]>[0]> = F extends [
(arg: any) => infer B
]
? [(arg: PrevReturn) => B]
: F extends [(arg: any) => infer B, ...infer Tail]
? Tail extends AnyFunc[]
? [(arg: PrevReturn) => B, ...PipeArgs<Tail, B>]
: []
: [];
export type ReduceAsync<T = unknown, V = T, R = V> = CurriedFunction3<
T[],
(accumulator: V | R, current: Awaited<T>, index: number) => R | Promise<R>,
V,
Promise<R>
>;
export function pipeAsync<F extends AnyFunc[], FirstFn extends F[0]>(
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
) {
type Args = Parameters<FirstFn>;
type ReturnT = F extends [...AnyFunc[], (...arg: any) => infer R]
? R extends Promise<infer R>
? R
: R
: never;
type CurriedMapAsync<T = unknown, R = unknown> = CurriedFunction3<
T[],
MapFunc<T, R>,
MapOptions,
Promise<R[]>
>;
const [firstFn, ...fnRest] = fns;
interface Method {
(...args: any[]): any;
}
return async (...args: Args): Promise<ReturnT> => {
let res: ReturnT = firstFn(args);
function pipeAsync(...methods: Method[]) {
return async (data: unknown) => {
let res = data;
for (let i = 0; i < methods.length; i += 1) {
res = await methods[i](res);
for (let i = 0; i < fnRest.length; i += 1) {
res = await fnRest[i](res);
}
return res;
};
}
const mapAsync: CurriedMapAsync = curry(pMap);
export const mapAsync = curry(pMap);
const reduceAsync: ReduceAsync = curryN(2, async (mixedArray, iteratee, initialValue) => {
let acc = initialValue;
for (let i = 0; i < mixedArray.length; i += 1) {
acc = await iteratee(acc, await mixedArray[i], i);
}
return acc;
});
export const reduceAsync =
(mixedArray: any[]) =>
async <T>(iteratee: AnyFunc, initialValue?: T) => {
let acc = initialValue;
for (let i = 0; i < mixedArray.length; i += 1) {
acc = await iteratee(acc, await mixedArray[i], i);
}
return acc;
};
const forEachAsync = curry(
async <T = unknown, R = unknown>(array: T[], func: MapFunc<T, R>, options: MapOptions) => {
await mapAsync(array, func, options);
}
);
export { mapAsync, reduceAsync, forEachAsync, pipeAsync };
export const forEachAsync = async <T, R>(
array: T[],
func: pMap.Mapper<T, R>,
options: pMap.Options
) => {
await pMap(array, func, options);
};

View File

@ -101,20 +101,20 @@ const isSingleType = ({ kind = COLLECTION_TYPE }) => kind === SINGLE_TYPE;
const isCollectionType = ({ kind = COLLECTION_TYPE }) => kind === COLLECTION_TYPE;
const isKind = (kind: Kind) => (model: Model) => model.kind === kind;
const getStoredPrivateAttributes = (model) =>
const getStoredPrivateAttributes = (model: Model) =>
union(
strapi?.config?.get('api.responses.privateAttributes', []) ?? [],
(strapi?.config?.get('api.responses.privateAttributes', []) ?? []) as Array<string>,
getOr([], 'options.privateAttributes', model)
);
const getPrivateAttributes = (model = {}) => {
const getPrivateAttributes = (model: Model) => {
return _.union(
getStoredPrivateAttributes(model),
_.keys(_.pickBy(model.attributes, (attr) => !!attr.private))
);
};
const isPrivateAttribute = (model, attributeName) => {
const isPrivateAttribute = (model: Model, attributeName: string) => {
if (model?.attributes?.[attributeName]?.private === true) {
return true;
}

View File

@ -34,16 +34,16 @@ const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
type SortOrder = 'asc' | 'desc';
interface SortMap {
export interface SortMap {
[key: string]: SortOrder | SortMap;
}
type SortQuery = string | string[] | object;
type FieldsQuery = string | string[];
interface FiltersQuery {}
type PopulateParams = {
export interface FiltersQuery {}
export interface PopulateParams {
sort?: SortQuery;
fields?: FieldsQuery;
filters?: FiltersQuery;
@ -51,11 +51,11 @@ type PopulateParams = {
on: {
[key: string]: PopulateParams;
};
};
}
type PopulateQuery = string | string[] | PopulateParams;
type PopulateQuery = boolean | string | string[] | PopulateParams;
interface Query {
export interface Query {
sort?: SortQuery;
fields?: FieldsQuery;
filters?: FiltersQuery;
@ -67,15 +67,18 @@ interface Query {
start?: number | string;
page?: number | string;
pageSize?: number | string;
publicationState?: 'live' | 'preview';
}
interface ConvertedQuery {
export interface ConvertedQuery {
orderBy?: SortQuery;
select?: FieldsQuery;
where?: FiltersQuery;
// NOTE: those are internal DB filters do not modify
filters?: any;
populate?: PopulateQuery;
count: boolean;
ordering: unknown;
count?: boolean;
ordering?: unknown;
_q?: string;
limit?: number;
offset?: number;
@ -248,7 +251,11 @@ class InvalidPopulateError extends Error {
}
// NOTE: we could support foo.* or foo.bar.* etc later on
const convertPopulateQueryParams = (populate: PopulateQuery, schema: Model, depth = 0) => {
const convertPopulateQueryParams = (
populate: PopulateQuery,
schema: Model,
depth = 0
): PopulateQuery => {
if (depth === 0 && populate === '*') {
return true;
}
@ -539,8 +546,12 @@ const convertAndSanitizeFilters = (filters: FiltersQuery, schema: Model) => {
return filters;
};
const convertPublicationStateParams = (type, params = {}, query = {}) => {
if (!type) {
const convertPublicationStateParams = (
schema: Model,
params: { publicationState?: 'live' | 'preview' } = {},
query: ConvertedQuery = {}
) => {
if (!schema) {
return;
}
@ -554,7 +565,7 @@ const convertPublicationStateParams = (type, params = {}, query = {}) => {
}
// NOTE: this is the query layer filters not the entity service filters
query.filters = ({ meta }) => {
query.filters = ({ meta }: { meta: Model }) => {
if (publicationState === 'live' && has(PUBLISHED_AT_ATTRIBUTE, meta.attributes)) {
return { [PUBLISHED_AT_ATTRIBUTE]: { $notNull: true } };
}
@ -613,7 +624,7 @@ const transformParamsToQuery = (uid: string, params: Query) => {
return query;
};
export = {
export default {
convertSortQueryParams,
convertStartQueryParams,
convertLimitQueryParams,

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
function env<T>(key: string, defaultValue?: T): string | T | undefined {
function envFn<T>(key: string, defaultValue?: T): string | T | undefined {
return _.has(process.env, key) ? process.env[key] : defaultValue;
}
@ -9,24 +9,24 @@ function getKey(key: string) {
}
const utils = {
int(key: string, defaultValue?: number): number {
if (!_.has(process.env, key) && defaultValue) {
int(key: string, defaultValue?: number): number | undefined {
if (!_.has(process.env, key)) {
return defaultValue;
}
return parseInt(getKey(key), 10);
},
float(key: string, defaultValue?: number): number {
if (!_.has(process.env, key) && defaultValue) {
float(key: string, defaultValue?: number): number | undefined {
if (!_.has(process.env, key)) {
return defaultValue;
}
return parseFloat(getKey(key));
},
bool(key: string, defaultValue?: boolean): boolean {
if (!_.has(process.env, key) && defaultValue) {
bool(key: string, defaultValue?: boolean): boolean | undefined {
if (!_.has(process.env, key)) {
return defaultValue;
}
@ -34,7 +34,7 @@ const utils = {
},
json(key: string, defaultValue?: object) {
if (!_.has(process.env, key) && defaultValue) {
if (!_.has(process.env, key)) {
return defaultValue;
}
@ -49,8 +49,8 @@ const utils = {
}
},
array(key: string, defaultValue?: string[]): string[] {
if (!_.has(process.env, key) && defaultValue) {
array(key: string, defaultValue?: string[]): string[] | undefined {
if (!_.has(process.env, key)) {
return defaultValue;
}
@ -65,8 +65,8 @@ const utils = {
});
},
date(key: string, defaultValue?: Date): Date {
if (!_.has(process.env, key) && defaultValue) {
date(key: string, defaultValue?: Date): Date | undefined {
if (!_.has(process.env, key)) {
return defaultValue;
}
@ -94,6 +94,6 @@ const utils = {
},
};
Object.assign(env, utils);
const env = Object.assign(envFn, utils);
export = env;
export default env;

View File

@ -24,7 +24,7 @@ class ValidationError extends ApplicationError {
}
class YupValidationError extends ValidationError {
constructor(yupError: yup.ValidationError, message: string) {
constructor(yupError: yup.ValidationError, message?: string) {
super('Validation');
const { errors, message: yupMessage } = formatYupErrors(yupError);
this.message = message || yupMessage;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
export = function importDefault(modName: string) {
export default function importDefault(modName: string) {
const mod = require(modName);
return mod && mod.__esModule ? mod.default : mod;
};

View File

@ -1,16 +1,13 @@
'use strict';
/**
* Export shared utilities
*/
const { buildQuery, hasDeepFilters } = require('./build-query');
const parseMultipartData = require('./parse-multipart');
const parseType = require('./parse-type');
const policy = require('./policy');
const templateConfiguration = require('./template-configuration');
const { yup, handleYupError, validateYupSchema, validateYupSchemaSync } = require('./validators');
const errors = require('./errors');
const {
import parseMultipartData from './parse-multipart';
import parseType from './parse-type';
import * as policy from './policy';
import templateConfiguration from './template-configuration';
import { yup, handleYupError, validateYupSchema, validateYupSchemaSync } from './validators';
import * as errors from './errors';
import {
nameToSlug,
nameToCollectionName,
getCommonBeginning,
@ -23,33 +20,31 @@ const {
startsWithANumber,
joinBy,
toKebabCase,
} = require('./string-formatting');
const { removeUndefined, keysDeep } = require('./object-formatting');
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
const { generateTimestampCode } = require('./code-generator');
const contentTypes = require('./content-types');
const env = require('./env-helper');
const relations = require('./relations');
const setCreatorFields = require('./set-creator-fields');
const hooks = require('./hooks');
const providerFactory = require('./provider-factory');
const pagination = require('./pagination');
const sanitize = require('./sanitize');
const traverseEntity = require('./traverse-entity');
const { pipeAsync, mapAsync, reduceAsync, forEachAsync } = require('./async');
const convertQueryParams = require('./convert-query-params');
const importDefault = require('./import-default');
const template = require('./template');
const file = require('./file');
const traverse = require('./traverse');
} from './string-formatting';
import { removeUndefined, keysDeep } from './object-formatting';
import { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } from './config';
import { generateTimestampCode } from './code-generator';
import * as contentTypes from './content-types';
import env from './env-helper';
import * as relations from './relations';
import setCreatorFields from './set-creator-fields';
import * as hooks from './hooks';
import providerFactory from './provider-factory';
import * as pagination from './pagination';
import sanitize from './sanitize';
import traverseEntity from './traverse-entity';
import { pipeAsync, mapAsync, reduceAsync, forEachAsync } from './async';
import convertQueryParams from './convert-query-params';
import importDefault from './import-default';
import * as template from './template';
import * as file from './file';
import * as traverse from './traverse';
module.exports = {
export {
yup,
handleYupError,
policy,
templateConfiguration,
buildQuery,
hasDeepFilters,
parseMultipartData,
sanitize,
traverseEntity,
@ -92,3 +87,53 @@ module.exports = {
file,
traverse,
};
const utils = {
yup,
handleYupError,
policy,
templateConfiguration,
parseMultipartData,
sanitize,
traverseEntity,
parseType,
nameToSlug,
toRegressedEnumValue,
startsWithANumber,
joinBy,
nameToCollectionName,
getCommonBeginning,
getConfigUrls,
escapeQuery,
removeUndefined,
keysDeep,
getAbsoluteAdminUrl,
getAbsoluteServerUrl,
generateTimestampCode,
stringIncludes,
stringEquals,
template,
isKebabCase,
isCamelCase,
toKebabCase,
contentTypes,
env,
relations,
setCreatorFields,
hooks,
providerFactory,
pagination,
pipeAsync,
mapAsync,
reduceAsync,
forEachAsync,
errors,
validateYupSchema,
validateYupSchemaSync,
convertQueryParams,
importDefault,
file,
traverse,
};
export default utils;

View File

@ -53,7 +53,10 @@ const withNoLimit = (pagination: Pagination, maxLimit = -1) => ({
limit: pagination.limit === -1 ? maxLimit : pagination.limit,
});
const withDefaultPagination = (args: PaginationArgs, { defaults = {}, maxLimit = -1 } = {}) => {
const withDefaultPagination = (
args: Partial<PaginationArgs>,
{ defaults = {}, maxLimit = -1 } = {}
) => {
const defaultValues = merge(STRAPI_DEFAULTS, defaults);
const usePagePagination = !isNil(args.page) || !isNil(args.pageSize);
@ -87,7 +90,7 @@ const withDefaultPagination = (args: PaginationArgs, { defaults = {}, maxLimit =
if (usePagePagination) {
const { page, pageSize } = merge(defaultValues.page, {
...args,
pageSize: Math.max(1, args.pageSize),
pageSize: Math.max(1, args.pageSize ?? 0),
});
Object.assign(pagination, {

View File

@ -73,7 +73,7 @@ type TypeMap = {
datetime: Date;
};
interface ParseTypeOptions {
export interface ParseTypeOptions {
type: keyof TypeMap;
value: any;
forceCast?: boolean;

View File

@ -92,7 +92,7 @@ const findPolicy = (name: string, policyContext: PolicyContext) => {
throw new Error(`Could not find policy "${name}"`);
};
const getPolicy = (policyConfig: PolicyConfig, policyContext: PolicyContext) => {
const getPolicy = (policyConfig: PolicyConfig, policyContext?: PolicyContext) => {
const { pluginName, apiName } = policyContext ?? {};
if (typeof policyConfig === 'function') {

View File

@ -22,7 +22,7 @@ const createProviderHooksMap = () => ({
didDelete: createAsyncParallelHook(),
});
interface Options {
export interface Options {
throwOnDuplicates?: boolean;
}
@ -113,4 +113,4 @@ const providerFactory = (options: Options = {}) => {
};
};
export = providerFactory;
export default providerFactory;

View File

@ -11,7 +11,7 @@ import traverseEntity, { Data } from '../traverse-entity';
import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from '../traverse';
import { Model } from '../types';
interface Options {
export interface Options {
auth?: unknown;
}
@ -45,7 +45,7 @@ const createContentAPISanitizers = () => {
.get('content-api.input')
.forEach((sanitizer: Sanitizer) => transforms.push(sanitizer(schema)));
return pipeAsync(...transforms)(data);
return pipeAsync(...transforms)(data as Data);
};
const sanitizeOutput: SanitizeFunc = async (data, schema: Model, { auth } = {}) => {
@ -68,7 +68,7 @@ const createContentAPISanitizers = () => {
.get('content-api.output')
.forEach((sanitizer: Sanitizer) => transforms.push(sanitizer(schema)));
return pipeAsync(...transforms)(data);
return pipeAsync(...transforms)(data as Data);
};
const sanitizeQuery = async (

View File

@ -3,7 +3,7 @@ import * as contentTypes from './content-types';
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = contentTypes.constants;
interface Options {
export interface Options {
user: User;
isEdition?: boolean;
}
@ -12,7 +12,7 @@ interface User {
id: string | number;
}
export = ({ user, isEdition = false }: Options) =>
export default ({ user, isEdition = false }: Options) =>
(data: object) => {
if (isEdition) {
return assoc(UPDATED_BY_ATTRIBUTE, user.id, data);

View File

@ -26,7 +26,7 @@ const getCommonPath = (...paths: string[]) => {
);
};
const escapeQuery = (query: string, charsToEscape: string[], escapeChar = '\\') => {
const escapeQuery = (query: string, charsToEscape: string, escapeChar = '\\') => {
return query
.split('')
.reduce(

View File

@ -26,7 +26,7 @@ export interface Path {
attribute: string | null;
}
interface TraverseOptions {
export interface TraverseOptions {
path?: Path;
schema: Model;
}

View File

@ -70,10 +70,6 @@ class StrapiIDSchema extends MixedSchemaType {
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
yup.strapiID = () => new StrapiIDSchema();
const handleYupError = (error: yup.ValidationError, errorMessage: string) => {
throw new YupValidationError(error, errorMessage);
};
@ -140,4 +136,14 @@ yup.setLocale({
},
});
export { yup, handleYupError, validateYupSchema, validateYupSchemaSync };
const customYup = Object.assign(yup, {
strapiID: (): InstanceType<typeof StrapiIDSchema> => new StrapiIDSchema(),
});
export {
customYup as yup,
StrapiIDSchema,
handleYupError,
validateYupSchema,
validateYupSchemaSync,
};