import { AuthController, Entity, EntityCollection, EntityReference, getPropertyInPath, isPropertyBuilder, mergeDeep, NavigationController, Properties, Property, PropertyOrBuilder, ResolvedProperty, resolveProperty } from "@firecms/core"; import { unflattenObject } from "./transforms"; import { isPrototypePollutingKey } from "./prototype_keys"; import { parseBooleanText, parseDateText, parseNumberText } from "./text_values"; import { getIn } from "@firecms/formex"; import { inferTypeFromValue } from "@firecms/schema_inference"; export function convertDataToEntity(authController: AuthController, navigation: NavigationController, data: Record, idColumn: string | undefined, headersMapping: Record, properties: Properties, path: string, defaultValues: Record): Entity { const flatObject = flattenEntry(data); if (idColumn) delete flatObject[idColumn]; const mappedKeysObject = Object.entries(flatObject) .map(([key, value]) => { const mappedKey = getIn(headersMapping, key) ?? key; const mappedProperty = getPropertyInPath(properties, mappedKey); if (!mappedProperty) { return {}; } const processedValue = processValueMapping(authController, value, navigation, mappedProperty); return ({ [mappedKey]: processedValue }); }) .reduce((acc, curr) => ({ ...acc, ...curr }), {}); const values = mergeDeep(defaultValues ?? {}, unflattenObject(mappedKeysObject)); let id = idColumn ? data[idColumn] : undefined; if (typeof id === "string") { id = id.trim(); } else if (typeof id === "number") { id = id.toString(); } else if (typeof id === "boolean") { id = id.toString(); } else if (id instanceof Date) { id = id.toISOString(); } else if (id && "toString" in id) { id = id.toString(); } return { id, values, path }; } export function flattenEntry(obj: any, parent = ""): any { return Object.keys(obj).reduce((acc, key) => { // Keys of uploaded JSON: `acc["__proto__"] = value` would replace the // accumulator's prototype instead of adding a field. if (isPrototypePollutingKey(key)) return acc; const prefixedKey = parent ? `${parent}.${key}` : key; if (typeof obj[key] === "object" && !(obj[key] instanceof Date) && obj[key] !== null && !Array.isArray(obj[key])) { Object.assign(acc, flattenEntry(obj[key], prefixedKey)); } else { // @ts-ignore acc[prefixedKey] = obj[key]; } return acc; }, {}); } export function processValueMapping(authController: AuthController, value: any, navigation: NavigationController, property?: PropertyOrBuilder): any { if (value === null) return null; if (property === undefined) return value; const usedProperty: ResolvedProperty | null = resolveProperty({ propertyOrBuilder: property, authController }) if (usedProperty === null) return value; const from = inferTypeFromValue(value); const to = usedProperty.dataType; if (from === "array" && to === "array" && Array.isArray(value) && usedProperty.of && !isPropertyBuilder(usedProperty.of as PropertyOrBuilder)) { return value.map(v => processValueMapping(authController, v, navigation, usedProperty.of as Property)); } else if (from === "string" && to === "number" && typeof value === "string") { return parseNumberText(value); } else if (from === "string" && to === "array" && typeof value === "string" && usedProperty.of && !isPropertyBuilder(usedProperty.of as PropertyOrBuilder)) { return value.split(",").map((v: string) => processValueMapping(authController, v.trim(), navigation, usedProperty.of as Property)); } else if (from === "string" && to === "boolean") { // Unrecognised text stays false, as it always was. return parseBooleanText(value) ?? false; } else if (from === "number" && to === "boolean") { return value === 1; } else if (from === "boolean" && to === "number") { return value ? 1 : 0; } else if (from === "boolean" && to === "string") { return value ? "true" : "false"; } else if (from === "number" && to === "string" && typeof value === "number") { return value.toString(); } else if (from === "string" && to === "array" && typeof value === "string") { return value.split(",").map((v: string) => v.trim()); } else if (from === "string" && to === "date" && typeof value === "string") { return parseDateText(value); } else if (from === "date" && to === "string") { return value instanceof Date && value.toISOString(); } else if (from === "number" && to === "date" && typeof value === "number") { try { return new Date(value); } catch (e) { return value; } } else if (from === "string" && to === "reference" && typeof value === "string") { // Parse optional database name in format: database_name:::path/to/entity // or simple format: path/to/entity let databaseId: string | undefined = undefined; let referencePath = value; if (value.includes(":::")) { const [dbName, pathPart] = value.split(":::"); if (dbName && dbName !== "(default)") { databaseId = dbName; } referencePath = pathPart; } // split value into path and entityId (entityId is the last part of the path, after the last /) const path = referencePath.split("/").slice(0, -1).join("/"); const entityId = referencePath.split("/").slice(-1)[0]; // If no explicit database was provided in the string, try to get it from the collection if (databaseId === undefined) { const targetCollection: EntityCollection | undefined = navigation.getCollectionById(path); databaseId = targetCollection?.databaseId; } return new EntityReference(entityId, path, databaseId); } else if (from === to) { return value; } else if (from === "array" && to === "string" && Array.isArray(value)) { return value.join(","); } return value; }