UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

462 lines (432 loc) 17.3 kB
import { ArrayProperty, AuthController, CMSType, CustomizationController, EntityCollection, EntityCustomView, EntityValues, EnumValueConfig, EnumValues, NumberProperty, Properties, PropertiesOrBuilders, Property, PropertyConfig, PropertyOrBuilder, ResolvedArrayProperty, ResolvedEntityCollection, ResolvedNumberProperty, ResolvedProperties, ResolvedProperty, ResolvedStringProperty, StringProperty, UserConfigurationPersistence } from "../types"; import { getValueInPath, mergeDeep } from "./objects"; import { getDefaultValuesFor, isPropertyBuilder } from "./entities"; import { DEFAULT_ONE_OF_TYPE } from "./common"; import { getIn } from "@firecms/formex"; import { enumToObjectEntries } from "./enums"; import { isDefaultFieldConfigId } from "../core"; export const resolveCollection = <M extends Record<string, any>, > ({ collection, path, entityId, values, previousValues, userConfigPersistence, propertyConfigs, ignoreMissingFields = false, authController }: { collection: EntityCollection<M> | ResolvedEntityCollection<M>; path: string, entityId?: string, values?: Partial<EntityValues<M>>, previousValues?: Partial<EntityValues<M>>, userConfigPersistence?: UserConfigurationPersistence; propertyConfigs?: Record<string, PropertyConfig>; ignoreMissingFields?: boolean; authController: AuthController; }): ResolvedEntityCollection<M> => { const collectionOverride = userConfigPersistence?.getCollectionConfig<M>(path); const storedProperties = getValueInPath(collectionOverride, "properties"); const defaultValues = getDefaultValuesFor(collection.properties); const usedValues = values ?? defaultValues; const usedPreviousValues = previousValues ?? values ?? defaultValues; const resolvedProperties = Object.entries(collection.properties) .map(([key, propertyOrBuilder]) => { const childResolvedProperty = resolveProperty({ propertyKey: key, propertyOrBuilder: propertyOrBuilder as PropertyOrBuilder | ResolvedProperty, values: usedValues, previousValues: usedPreviousValues, path, entityId, propertyConfigs: propertyConfigs, ignoreMissingFields, authController }); if (!childResolvedProperty) return {}; return ({ [key]: childResolvedProperty }); }) .filter((a) => a !== null) .reduce((a, b) => ({ ...a, ...b }), {}) as ResolvedProperties<M>; const properties: Properties = mergeDeep(resolvedProperties, storedProperties); const cleanedProperties = Object.entries(properties) .filter(([_, property]) => Boolean(property?.dataType)) .map(([id, property]) => ({ [id]: property })) .reduce((a, b) => ({ ...a, ...b }), {}); return { ...collection, properties: cleanedProperties, originalCollection: collection } as ResolvedEntityCollection<M>; }; /** * Resolve property builders, enums and arrays. * @param propertyOrBuilder * @param propertyValue */ export function resolveProperty<T extends CMSType = CMSType, M extends Record<string, any> = any>({ propertyOrBuilder, fromBuilder = false, ignoreMissingFields = false, ...props }: { propertyKey?: string, propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T>, values?: Partial<M>, previousValues?: Partial<M>, path?: string, entityId?: string, index?: number, fromBuilder?: boolean; propertyConfigs?: Record<string, PropertyConfig<any>>; ignoreMissingFields?: boolean; authController: AuthController; }): ResolvedProperty<T> | null { if (typeof propertyOrBuilder === "object" && "resolved" in propertyOrBuilder) { return propertyOrBuilder as ResolvedProperty<T>; } let resolvedProperty: ResolvedProperty<T> | null = null; if (!propertyOrBuilder) { return null; } else if (isPropertyBuilder(propertyOrBuilder)) { const path = props.path; if (!path) throw Error("Trying to resolve a property builder without specifying the entity path"); const usedPropertyValue = props.propertyKey ? getIn(props.values, props.propertyKey) : undefined; const result: Property<T> | null = propertyOrBuilder({ ...props, path, propertyValue: usedPropertyValue, values: props.values ?? {}, previousValues: props.previousValues ?? props.values ?? {} }); if (!result) { return null; } resolvedProperty = resolveProperty({ ...props, propertyOrBuilder: result, fromBuilder: true, ignoreMissingFields }); } else { const property = propertyOrBuilder as Property<T>; if (property.dataType === "map" && property.properties) { const properties = resolveProperties({ ignoreMissingFields, ...props, properties: property.properties, }); resolvedProperty = { ...property, resolved: true, fromBuilder, properties } as ResolvedProperty<T>; } else if (property.dataType === "array") { resolvedProperty = resolveArrayProperty({ property, fromBuilder, ignoreMissingFields, ...props }) as ResolvedProperty<any>; } else if ((property.dataType === "string" || property.dataType === "number") && property.enumValues) { resolvedProperty = resolvePropertyEnum(property, fromBuilder) as ResolvedProperty<any>; } } if (!resolvedProperty) { resolvedProperty = { ...propertyOrBuilder, resolved: true, fromBuilder } as ResolvedProperty<T>; } if (resolvedProperty.propertyConfig && !isDefaultFieldConfigId(resolvedProperty.propertyConfig)) { const cmsFields = props.propertyConfigs; if (!cmsFields && !ignoreMissingFields) { throw Error(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property configs were provided. Use the property 'propertyConfigs' in your app config to provide them`); } const customField: PropertyConfig | undefined = cmsFields?.[resolvedProperty.propertyConfig]; if (!customField) { console.warn(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property config with that key was found. Check the 'propertyConfigs' in your app config`) console.warn("Available property configs", cmsFields); return null; } if (customField.property) { const configPropertyOrBuilder = customField.property; if ("propertyConfig" in configPropertyOrBuilder) { delete configPropertyOrBuilder.propertyConfig; } const customFieldProperty = resolveProperty<any>({ propertyOrBuilder: configPropertyOrBuilder, ignoreMissingFields, ...props }); if (customFieldProperty) { resolvedProperty = mergeDeep(customFieldProperty, resolvedProperty); } } } return resolvedProperty ? { ...resolvedProperty, resolved: true } : null; } export function getArrayResolvedProperties<M>({ propertyKey, propertyValue, property, ...props }: { propertyValue: any, propertyKey?: string, property: ArrayProperty<any> | ResolvedArrayProperty<any>, ignoreMissingFields: boolean, values?: Partial<M>; previousValues?: Partial<M>; path?: string; entityId?: string; index?: number; fromBuilder?: boolean; propertyConfigs?: Record<string, PropertyConfig>; authController: AuthController; }) { const of = property.of; return Array.isArray(propertyValue) ? propertyValue.map((v: any, index: number) => { return resolveProperty({ propertyKey: `${propertyKey}.${index}`, propertyOrBuilder: of, ...props, index }); }).filter(e => Boolean(e)) as ResolvedProperty[] : []; } export function resolveArrayProperty<T extends any[], M>({ propertyKey, property, ignoreMissingFields = false, ...props }: { propertyKey?: string, property: ArrayProperty<T> | ResolvedArrayProperty<T>, values?: Partial<M>, previousValues?: Partial<M>, path?: string, entityId?: string, index?: number, fromBuilder?: boolean; propertyConfigs?: Record<string, PropertyConfig>; ignoreMissingFields?: boolean; authController: AuthController; }): ResolvedArrayProperty { const propertyValue = propertyKey ? getIn(props.values, propertyKey) : undefined; if (property.of) { if (Array.isArray(property.of)) { return { ...property, resolved: true, fromBuilder: props.fromBuilder, resolvedProperties: property.of.map((p, index) => { return resolveProperty({ propertyKey: `${propertyKey}.${index}`, propertyOrBuilder: p as Property<any>, ignoreMissingFields, ...props, index, }); }) } as ResolvedArrayProperty; } else { const of = property.of; const resolvedProperties = getArrayResolvedProperties({ propertyValue, propertyKey, property, ignoreMissingFields, ...props }); const ofProperty = resolveProperty({ propertyOrBuilder: of, ignoreMissingFields, ...props }); if (!ofProperty && !ignoreMissingFields) throw Error("When using a property builder as the 'of' prop of an ArrayProperty, you must return a valid child property") return { ...property, resolved: true, fromBuilder: props.fromBuilder, of: ofProperty, resolvedProperties } as ResolvedArrayProperty; } } else if (property.oneOf) { const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE; const resolvedProperties: ResolvedProperty[] = Array.isArray(propertyValue) ? propertyValue.map((v, index) => { const type = v && v[typeField]; const childProperty = property.oneOf?.properties[type]; if (!type || !childProperty) return null; return resolveProperty({ propertyKey: `${propertyKey}.${index}`, propertyOrBuilder: childProperty, ignoreMissingFields, ...props }); }).filter(e => Boolean(e)) as ResolvedProperty[] : []; const properties = resolveProperties<any>({ propertyKey, properties: property.oneOf.properties, ignoreMissingFields, ...props }); return { ...property, resolved: true, oneOf: { ...property.oneOf, properties }, fromBuilder: props.fromBuilder, resolvedProperties } as ResolvedArrayProperty; } else if (!property.Field) { throw Error("The array property needs to declare an 'of' or a 'oneOf' property, or provide a custom `Field`") } else { return { ...property, resolved: true, fromBuilder: props.fromBuilder } as ResolvedArrayProperty; } } /** * Resolve enums and arrays for properties * @param properties * @param value */ export function resolveProperties<M extends Record<string, any>>({ propertyKey, properties, ignoreMissingFields, ...props }: { propertyKey?: string, properties: PropertiesOrBuilders<M>, values?: Partial<M>, previousValues?: Partial<M>, path?: string, entityId?: string, index?: number, fromBuilder?: boolean; propertyConfigs?: Record<string, PropertyConfig>; ignoreMissingFields?: boolean; authController: AuthController; }): ResolvedProperties<M> { return Object.entries<PropertyOrBuilder>(properties as Record<string, PropertyOrBuilder>) .map(([key, property]) => { const childResolvedProperty = resolveProperty({ propertyKey: propertyKey ? `${propertyKey}.${key}` : undefined, propertyOrBuilder: property, ignoreMissingFields, ...props }); if (!childResolvedProperty) return {}; return { [key]: childResolvedProperty }; }) .filter((a) => a !== null) .reduce((a, b) => ({ ...a, ...b }), {}) as ResolvedProperties<M>; } /** * Resolve enum aliases for a string or number property * @param property * @param fromBuilder */ export function resolvePropertyEnum(property: StringProperty | NumberProperty, fromBuilder?: boolean): ResolvedStringProperty | ResolvedNumberProperty { if (typeof property.enumValues === "object") { return { ...property, resolved: true, enumValues: enumToObjectEntries(property.enumValues)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [], fromBuilder: fromBuilder ?? false } } return property as ResolvedStringProperty | ResolvedNumberProperty; } export function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined { if (typeof input === "object") { return Object.entries(input).map(([id, value]) => (typeof value === "string" ? { id, label: value } : value)); } else if (Array.isArray(input)) { return input as EnumValueConfig[]; } else { return undefined; } } export function resolveEntityView(entityView: string | EntityCustomView<any>, contextEntityViews?: EntityCustomView<any>[]): EntityCustomView<any> | undefined { if (typeof entityView === "string") { return contextEntityViews?.find((entry) => entry.key === entityView); } else { return entityView; } } export function resolvedSelectedEntityView<M extends Record<string, any>>( customViews: (string | EntityCustomView<M>)[] | undefined, customizationController: CustomizationController, selectedTab?: string, canEdit?: boolean, ) { const resolvedEntityViews = customViews ? customViews .map(e => resolveEntityView(e, customizationController.entityViews)) .filter((e): e is EntityCustomView<M> => Boolean(e)) // .filter((e) => canEdit || !e.includeActions) : []; const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab); const selectedSecondaryForm = customViews && resolvedEntityViews.filter(e => e.includeActions).find(e => e.key === selectedTab); return { resolvedEntityViews, selectedEntityView, selectedSecondaryForm }; }