lang/Schema.js

const { _ } = require('@kitmi/utils');
const { generateDisplayName, deepCloneField, Clonable, schemaNaming } = require('./XemlUtils');

/**
 * Geml schema class.
 * @class Schema
 */
class Schema extends Clonable {
    /**
     * Types in this schema, map of <typeName, typeInfo>
     * @member {object.<String, Object>}
     */
     types = {};

    /**
     * Entities in this schema, map of <entityName, entityObject>
     * @member {object.<string, Entity>}
     */
    entities = {};

    /**
     * Datasets, dataset = entity + relations + projection
     * @member {object}
     */
    datasets = {};

    /**
     * Views, view = dataset + filters 
     * @member {object}
     */
    views = {};    

    /**     
     * @param {Linker} linker
     * @param {string} name     
     * @param {object} info
     */
    constructor(linker, name, info) {
        super();

        /**
         * Linker to process this schema
         * @member {Linker}
         */
        this.linker = linker;

        /**
         * Name of this entity
         * @member {string}
         */
        this.name = schemaNaming(name);

        /**
         * Owner geml module
         * @member {object}
         */
        this.xemlModule = this.linker.entryModule;

        /**
         * Raw metadata
         * @member {object}
         */
        this.info = info;       
    }

    /**
     * Start linking this schema
     * @returns {Schema}
     */
    link() {
        pre: !this.linked;

        this.linker.log('verbose', 'Linking schema [' + this.name + '] ...');

        if (this.info.comment) {
            /**
             * @member {string}
             */
            this.comment = this.info.comment;
        }

        /**
         * @member {string}
         */
        this.displayName = generateDisplayName(this.name);

        //1st round, get direct output entities
        this.info.entities || (this.info.entities = []);

        this.info.entities.forEach(entityEntry => {            
            let entity = this.linker.loadEntity(this.xemlModule, entityEntry.entity);
            if (!entity.linked) {
                throw new Error(`Entity [${entity.name}] not linked after loading.`);
            }

            this.addEntity(entity);
        });

        if (!_.isEmpty(this.info.views)) {
            this.info.views.forEach(viewName => {
                let view = this.linker.loadView(this.xemlModule, viewName);
                if (!view.linked) {
                    throw new Error(`View [${entity.name}] not linked after loading.`);
                }

                this.addView(view);
            });
        }

        this.linked = true;

        return this;
    }

    /**
     * Add an type into the schema
     * @param {*} type 
     * @param {*} typeLocation 
     * @returns {Schema}
     */
    addType(type, typeLocation) {
        const existing = this.types[type];
        if (existing == null) {
            this.types[type] = typeLocation;
        } else {
            if (existing !== typeLocation) {
                //should never happen
                throw new Error('Different used types appear in the same entity!');
            }
        }

        return this;
    }

    /**
     * Check whether a entity with given name is in the schema
     * @param {string} entityName
     * @returns {boolean}
     */
    hasEntity(entityName) {
        return (entityName in this.entities);
    }

    /**
     * Add an entity into the schema
     * @param {Entity} entity
     * @returns {Schema}
     */
    addEntity(entity) {
        if (this.hasEntity(entity.name)) {
            throw new Error(`Entity name [${entity.name}] conflicts in schema [${this.name}].`);
        }

        this.entities[entity.name] = entity;

        _.each(entity.types, (info, type) => this.addType(type, info));

        return this;
    }

    /**
     * Check whether a view with given name is in the schema
     * @param {string} viewName
     * @returns {boolean}
     */
    hasView(viewName) {
        return (viewName in this.views);
    }

    /**
     * Add a view into the schema
     * @param {View} view 
     * @returns {Schema}
     */
    addView(view) {
        pre: !this.hasView(view.name), `View name [${view.name}] conflicts in schema [${this.name}].`;

        this.views[view.name] = view;

        return this;
    }

    /**
     * Get a document hierarchy
     * @param {object} fromModule
     * @param {string} datasetName
     * @returns {object}
     */
    getDocumentHierachy(fromModule, datasetName) {
        if (datasetName in this.datasets) {
            return this.datasets[datasetName];
        }

        let dataset = this.linker.loadDataset(fromModule, datasetName);
        return (this.datasets[datasetName] = dataset.buildHierarchy(this));
    }

    /**
     * Get the referenced entity, add it into schema if not in schema
     * @param {object} refererModule
     * @param {string} entityName
     * @returns {Entity}
     */
    getReferencedEntity(refererModule, entityName) {
        let entity = this.linker.loadEntity(refererModule, entityName);

        if (!this.hasEntity(entity.name)) {
            throw new Error(`Entity "${entity.name}" not exists in schema "${this.name}".`);
        }

        return entity;
    }

    /**
     * 
     * @param {*} refererModule 
     * @param {*} entityName 
     */
    ensureGetEntity(refererModule, entityName, newlyAdded) {
        if (this.hasEntity(entityName)) return this.entities[entityName];

        let entity = this.linker.loadEntity(refererModule, entityName, false);

        if (entity) {
            this.addEntity(entity);   

            if (!entity.info.abstract && newlyAdded) {
                newlyAdded.push(entity.name);
                this.linker.log('info', `New entity "${entity.name}" added by association.`);
            }
        }

        return entity;
    }

    /**
     * Clone the schema
     * @returns {Schema}
     */
    clone() {
        super.clone();
        
        let schema = new Schema(this.linker, this.name, this.info);
        
        deepCloneField(this, schema, 'displayName');
        deepCloneField(this, schema, 'comment');        
        deepCloneField(this, schema, 'entities');   
        deepCloneField(this, schema, 'types');        
        deepCloneField(this, schema, 'datasets');
        deepCloneField(this, schema, 'views');        

        schema.linked = true;

        return schema;
    }

    /**
     * Translate the schema into a plain JSON object
     * @returns {object}
     */
    toJSON() {
        return {
            name: this.name,
            displayName: this.displayName,
            comment: this.comment,        
            entities: _.mapValues(this.entities, entity => entity.toJSON()),   
            types: this.types,         
            datasets: _.mapValues(this.datasets, dataset => dataset.toJSON()), 
            views: _.mapValues(this.views, view => view.toJSON()) 
        };
    }
}

module.exports = Schema;