UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

403 lines (382 loc) 18 kB
import { CMSType, EntityReference, GeoPoint, ResolvedArrayProperty, ResolvedMapProperty, ResolvedProperties, ResolvedProperty } from "../types"; import * as yup from "yup"; import { AnySchema, ArraySchema, BooleanSchema, DateSchema, NumberSchema, ObjectSchema, StringSchema } from "yup"; import { enumToObjectEntries, getValueInPath, hydrateRegExp, isPropertyBuilder } from "../util"; // Add custom unique function for array values declare module "yup" { // tslint:disable-next-line interface ArraySchema<T> { uniqueInArray(mapper: (a: T) => T, message: string): ArraySchema<T>; } } yup.addMethod(yup.array, "uniqueInArray", function ( mapper = (a: any) => a, message: string ) { return this.test("uniqueInArray", message, values => { return !values || values.length === new Set(values.map(mapper)).size; }); }); export type CustomFieldValidator = (props: { name: string, value: any, property: ResolvedProperty, entityId?: string, parentProperty?: ResolvedMapProperty | ResolvedArrayProperty, }) => Promise<boolean>; interface PropertyContext<T extends CMSType> { property: ResolvedProperty<T>, parentProperty?: ResolvedMapProperty | ResolvedArrayProperty, entityId: string, customFieldValidator?: CustomFieldValidator, name?: any } export function getYupEntitySchema<M extends Record<string, any>>( entityId: string, properties: ResolvedProperties<M>, customFieldValidator?: CustomFieldValidator): ObjectSchema<any> { const objectSchema: any = {}; Object.entries(properties as Record<string, ResolvedProperty>) .forEach(([name, property]) => { objectSchema[name] = mapPropertyToYup({ property: property as ResolvedProperty<any>, customFieldValidator, name, entityId }); }); return yup.object().shape(objectSchema); } export function mapPropertyToYup<T extends CMSType>(propertyContext: PropertyContext<T>): AnySchema<unknown> { const property = propertyContext.property; if (isPropertyBuilder(property)) { console.error("Error in property", propertyContext); throw Error("Trying to create a yup mapping from a property builder. Please use resolved properties only"); } if (property.dataType === "string") { return getYupStringSchema(propertyContext as PropertyContext<string>); } else if (property.dataType === "number") { return getYupNumberSchema(propertyContext as PropertyContext<number>); } else if (property.dataType === "boolean") { return getYupBooleanSchema(propertyContext as PropertyContext<boolean>); } else if (property.dataType === "map") { return getYupMapObjectSchema(propertyContext as PropertyContext<object>); } else if (property.dataType === "array") { return getYupArraySchema(propertyContext as PropertyContext<any[]>); } else if (property.dataType === "date") { return getYupDateSchema(propertyContext as PropertyContext<Date>); } else if (property.dataType === "geopoint") { return getYupGeoPointSchema(propertyContext as PropertyContext<GeoPoint>); } else if (property.dataType === "reference") { return getYupReferenceSchema(propertyContext as PropertyContext<EntityReference>); } console.error("Unsupported data type in yup mapping", property) throw Error("Unsupported data type in yup mapping"); } export function getYupMapObjectSchema({ property, entityId, customFieldValidator, name }: PropertyContext<Record<string, any>>): ObjectSchema<any> { const objectSchema: any = {}; const validation = property.validation; if (property.properties) Object.entries(property.properties).forEach(([childName, childProperty]: [string, ResolvedProperty]) => { objectSchema[childName] = mapPropertyToYup<any>({ property: childProperty, parentProperty: property as ResolvedMapProperty, customFieldValidator, name: `${name}[${childName}]`, entityId }); }); const shape = yup.object().shape(objectSchema); if (validation?.required) { return shape.required(validation?.requiredMessage ? validation.requiredMessage : "Required").nullable(true); } return yup.object().shape(shape.fields).default(undefined).notRequired().nullable(true); } function getYupStringSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<string>): StringSchema { let collection: StringSchema<any> = yup.string(); const validation = property.validation; if (property.enumValues) { if (validation?.required) collection = collection.required(validation?.requiredMessage ? validation.requiredMessage : "Required"); const entries = enumToObjectEntries(property.enumValues); collection = collection.oneOf( (validation?.required ? entries : [...entries, null]) .map((enumValueConfig) => enumValueConfig?.id ?? null) ).nullable(true); } if (validation) { collection = validation.required ? collection.required(validation?.requiredMessage ? validation.requiredMessage : "Required").nullable(true) : collection.notRequired().nullable(true); if (validation.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value, context) => customFieldValidator({ name, property, parentProperty, value, entityId })); if (validation.min || validation.min === 0) collection = collection.min(validation.min, `${property.name} must be min ${validation.min} characters long`); if (validation.max || validation.max === 0) collection = collection.max(validation.max, `${property.name} must be max ${validation.max} characters long`); if (validation.matches) { const regExp = typeof validation.matches === "string" ? hydrateRegExp(validation.matches) : validation.matches; if (regExp) { collection = collection.matches(regExp, validation.matchesMessage ? { message: validation.matchesMessage } : undefined) } } if (validation.trim) collection = collection.trim(); if (validation.lowercase) collection = collection.lowercase(); if (validation.uppercase) collection = collection.uppercase(); if (property.email) collection = collection.email(`${property.name} must be an email`); if (property.url) { if (!property.storage || property.storage?.storeUrl) { collection = collection.url(`${property.name} must be a url`); } else { console.warn(`Property ${property.name} has a url validation but its storage configuration is not set to store urls`); } } } else { collection = collection.notRequired().nullable(true); } return collection; } function getYupNumberSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<number>): NumberSchema { const validation = property.validation; let collection: NumberSchema<any> = yup.number().typeError("Must be a number"); if (validation) { collection = validation.required ? collection.required(validation.requiredMessage ? validation.requiredMessage : "Required").nullable(true) : collection.notRequired().nullable(true); if (validation.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value) => customFieldValidator({ name, property, parentProperty, value, entityId })); if (validation.min || validation.min === 0) collection = collection.min(validation.min, `${property.name} must be higher or equal to ${validation.min}`); if (validation.max || validation.max === 0) collection = collection.max(validation.max, `${property.name} must be lower or equal to ${validation.max}`); if (validation.lessThan || validation.lessThan === 0) collection = collection.lessThan(validation.lessThan, `${property.name} must be higher than ${validation.lessThan}`); if (validation.moreThan || validation.moreThan === 0) collection = collection.moreThan(validation.moreThan, `${property.name} must be lower than ${validation.moreThan}`); if (validation.positive) collection = collection.positive(`${property.name} must be positive`); if (validation.negative) collection = collection.negative(`${property.name} must be negative`); if (validation.integer) collection = collection.integer(`${property.name} must be an integer`); } else { collection = collection.notRequired().nullable(true); } return collection; } function getYupGeoPointSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<GeoPoint>): AnySchema { let collection: ObjectSchema<any> = yup.object(); const validation = property.validation; if (validation?.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value) => customFieldValidator({ name, property, parentProperty, value, entityId })); if (validation?.required) { collection = collection.required(validation.requiredMessage).nullable(true); } else { collection = collection.notRequired().nullable(true); } return collection; } function getYupDateSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<Date>): AnySchema | DateSchema { if (property.autoValue) { return yup.object().nullable(); } let collection: DateSchema<any> = yup.date(); const validation = property.validation; if (validation) { collection = validation.required ? collection.required(validation?.requiredMessage ? validation.requiredMessage : "Required") : collection.notRequired(); if (validation.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value) => customFieldValidator({ name, property, parentProperty, value, entityId })); if (validation.min) collection = collection.min(validation.min, `${property.name} must be after ${validation.min}`); if (validation.max) collection = collection.max(validation.max, `${property.name} must be before ${validation.min}`); } else { collection = collection.notRequired(); } return collection .transform((v: any) => (v instanceof Date ? v : null)) .nullable(); } function getYupReferenceSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<EntityReference>): AnySchema { let collection: ObjectSchema<any> = yup.object(); const validation = property.validation; if (validation) { collection = validation.required ? collection.required(validation?.requiredMessage ? validation.requiredMessage : "Required").nullable(true) : collection.notRequired().nullable(true); if (validation.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value) => customFieldValidator({ name, property, parentProperty, value, entityId })); } else { collection = collection.notRequired().nullable(true); } return collection; } function getYupBooleanSchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<boolean>): BooleanSchema { let collection: BooleanSchema<any> = yup.boolean(); const validation = property.validation; if (validation) { collection = validation.required ? collection.required(validation?.requiredMessage ? validation.requiredMessage : "Required").nullable(true) : collection.notRequired().nullable(true); if (validation.unique && customFieldValidator && name) collection = collection.test("unique", "This value already exists and should be unique", (value) => customFieldValidator({ name, property, parentProperty, value, entityId })); } else { collection = collection.notRequired().nullable(true); } return collection; } function hasUniqueInArrayModifier(property: ResolvedProperty): boolean | [string, ResolvedProperty][] { if (property.validation?.uniqueInArray) { return true; } else if (property.dataType === "map" && property.properties) { return Object.entries(property.properties) .filter(([key, childProperty]) => childProperty.validation?.uniqueInArray); } return false; } function getYupArraySchema({ property, parentProperty, customFieldValidator, name, entityId }: PropertyContext<any[]>): AnySchema<any> { let arraySchema: ArraySchema<any> = yup.array(); if (property.of) { if (Array.isArray(property.of)) { const yupProperties = (property.of as ResolvedProperty[]).map((p, index) => ({ [`${name}[${index}]`]: mapPropertyToYup({ property: p as ResolvedProperty<any>, parentProperty: property, entityId }) })).reduce((a, b) => ({ ...a, ...b }), {}); return yup.array().of( yup.mixed().test( "Dynamic object validation", "Dynamic object validation error", (object, context) => { const yupProperty = getValueInPath(yupProperties, context.path); return yupProperty.validate(object); } ) ); } else { arraySchema = arraySchema.of(mapPropertyToYup({ property: property.of, parentProperty: property, entityId })); const arrayUniqueFields = hasUniqueInArrayModifier(property.of); if (arrayUniqueFields) { if (typeof arrayUniqueFields === "boolean") { arraySchema = arraySchema.uniqueInArray(v => v, `${property.name} should have unique values within the array`); } else if (Array.isArray(arrayUniqueFields)) { arrayUniqueFields.forEach(([name, childProperty]) => { arraySchema = arraySchema.uniqueInArray(v => v && v[name], `${property.name}${childProperty.name ?? name}: should have unique values within the array`); } ); } } } } const validation = property.validation; if (validation) { arraySchema = validation.required ? arraySchema.required(validation?.requiredMessage ? validation.requiredMessage : "Required").nullable(true) : arraySchema.notRequired().nullable(true); if (validation.min || validation.min === 0) arraySchema = arraySchema.min(validation.min, `${property.name} should be min ${validation.min} entries long`); if (validation.max) arraySchema = arraySchema.max(validation.max, `${property.name} should be max ${validation.max} entries long`); } else { arraySchema = arraySchema.notRequired().nullable(true); } return arraySchema; }