funkjunky/schemaless-normalizer

View on GitHub
flatten.js

Summary

Maintainability
A
2 hrs
Test Coverage
import pluralize, { singulize } from './pluralize.js';
import getPropertyTree from './getPropertyTree.js';
import expandable from './expandable';
import forEachM2mRelation from './forEachM2mRelation';

// const flattenUsers = flatten('users')
// examples: yield put(mergeUsers(flattenUsers(unnormalizedUsers)))
// format:
//  {
//      assets: {
//          1: { id: 1, commits: [{ id: 7 }], ... },
//          2: { id: 2, ... },
//      },
//      commits: {
//          7: { id: 7, asset: { id: 1 } }
//      }
//  }
export default (modelName, config={}) => data => {
    const { models=[], oneToOne={}, keyToModel={}, manyToMany={} } = config;

    let result = {};

    const flattenModel = (modelName, model) => {
        if (models.length && !models.includes(modelName)) {
            console.warn('model name doesnt exist: ', modelName);
            return;
        }
        if (!model.id) return;

        // if first model of type modelName, then create the modelName hash
        if (!result[modelName])
            result[modelName] = {};

        // if it's the first model with this ide, then create the model object
        if (!result[modelName][model.id])
            result[modelName][model.id] = {};

        Object.keys(model).forEach((key) => {
            // if member is an object, then we need to extract it into our flattened data
            if (expandable(model, key, modelName, oneToOne)) {

                const mappedKey = getPropertyTree(keyToModel, key, modelName, key);
                // if member is an array, then extract all the models into flattened data
                if (Array.isArray(model[key])) {
                    // TODO: we should be able to assume a m2m relationship given an array of models, related to an array of models
                    // The challenge well be remembering this relationship. Because once it's in m2m, the models lose reference to the relationship.
                    const foundM2m = forEachM2mRelation(mappedKey, manyToMany, (m2mKey, relativeKey) => {
                        if (!result[m2mKey]) result[m2mKey] = {};
                        if (!result[m2mKey][mappedKey]) result[m2mKey][mappedKey] = {};
                        if (!result[m2mKey][relativeKey]) result[m2mKey][relativeKey] = {};
                        result[m2mKey][relativeKey][model.id] = {
                            id: model.id,
                            [mappedKey]: model[key].map(({ id }) => ({ id })),
                        };
                        model[key].forEach(({ id }) => {
                            // Only if we haven't added this models id to the m2m relationship...
                            if (!getPropertyTree(result[m2mKey][mappedKey], [], id, relativeKey).find(({ id }) => id === model.id)) {
                                // add this model to the m2m relationship.
                                result[m2mKey][mappedKey][id] = {
                                    id,
                                    [relativeKey]: [{ id: model.id }]
                                };
                            }
                        });
                    });

                    if (!foundM2m) {
                        result[modelName][model.id][key] = model[key].map(({ id }) => ({ id }));

                        let mappedModelKey = getMappedKey(keyToModel, mappedKey, singulize(modelName));
                        flattenArrayOfModels(mappedKey, model[key].map(m => {
                            if (!m[mappedModelKey] && !m[modelName]) {
                                return {
                                    [mappedModelKey]: { id: model.id },
                                    ...m   //The model takes precedence for data.
                                };
                            } else return m;
                        }));
                    // TODO: NOT DRY
                    } else {
                        let mappedModelKey = getMappedKey(keyToModel, mappedKey, singulize(modelName));
                        flattenArrayOfModels(mappedKey, model[key]);
                    }
                // Else it's a plain object, just recurse
                // Note: we only continue if we have data to add, otherwise we'd go infinite
                } else {
                    if (Object.keys(model) >= 1 || !result[pluralize(mappedKey)] || !result[pluralize(mappedKey)][model[key].id]) {
                        let mappedModelKey = getMappedKey(keyToModel, mappedKey, singulize(modelName));
                        if (!model[key][mappedModelKey] && !model[key][modelName]) {
                            flattenModel(pluralize(mappedKey), {
                                [mappedModelKey]: { id: model.id },
                                ...model[key],
                            });
                        } else {
                            flattenModel(pluralize(mappedKey), model[key]);
                        };
                    }
                    result[modelName][model.id][key] = { id: model[key].id };
                }
            } else {
                result[modelName][model.id][key] = model[key];
            }
        });
    };

    const flattenArrayOfModels = (modelName, models) => {
        models.forEach(model => {
            flattenModel(modelName, model);
        });
    };

    // This is where we start the flattening process.
    if (Array.isArray(data)) {
        flattenArrayOfModels(modelName, data);
    } else {
        flattenModel(modelName, data);
    }

    // We return the result of flattening, which is computer through recursion.
    return result;
};

// TODO: done quickly, prob not best solution.
const getMappedKey = (keyToModel, modelName, key) => {
    let mappedModelKey = key;
    const modelMappedKeys = keyToModel[modelName];
    if (modelMappedKeys) {
        Object.entries(modelMappedKeys).forEach(([mappedKey, original]) => {
            if (original === key) {
                mappedModelKey = mappedKey;
            }
        });
    }
    return mappedModelKey;
};