modeler/Dao.js

const path = require('node:path');
const { _, naming, text, isPlainObject, baseName, isEmpty, pushIntoBucket } = require('@kitmi/utils');
const { fs } = require('@kitmi/sys');
const swig = require('swig-templates');
const esprima = require('esprima');

const XemlTypes = require('../lang/XemlTypes');
const JsLang = require('./util/ast');
const XemlToAst = require('./util/xemlToAst');
const Snippets = require('./dao/snippets');
const { extractReferenceBaseName } = require('../lang/XemlUtils');

const ChainableType = [
    XemlToAst.AST_BLK_VALIDATOR_CALL,
    XemlToAst.AST_BLK_PROCESSOR_CALL,
    XemlToAst.AST_BLK_ACTIVATOR_CALL,
];

const getFieldName = (t) => t.split('.').pop();
const isChainable = (current, next) =>
    ChainableType.indexOf(current.type) > -1 && current.target === next.target && next.type === current.type;
const chainCall = (lastBlock, lastType, currentBlock, currentType) => {
    if (lastBlock) {
        if (lastType === 'ValidatorCall') {
            if (currentType !== 'ValidatorCall') {
                throw new Error('Unexpected currentType');
            } 

            currentBlock = JsLang.astBinExp(lastBlock, '&&', currentBlock);
        } else {
            if (currentType !== 'ProcessorCall') {
                console.log({
                    lastType,
                    currentType,
                    lastBlock: JsLang.astToCode(lastBlock),
                    currentBlock: JsLang.astToCode(currentBlock),
                });
                throw new Error('Unexpected currentType: ' + currentType + ' last: ' + lastType);
            }

            currentBlock.arguments[0] = lastBlock;
        }
    }

    return currentBlock;
};
const asyncMethodNaming = (name) => name + '_';

const indentLines = (lines, indentation) =>
    lines
        .split('\n')
        .map((line, i) => (i === 0 ? line : _.repeat(' ', indentation) + line))
        .join('\n');

const XEML_MODIFIER_RETURN = {
    [XemlTypes.Modifier.VALIDATOR]: () => [JsLang.astThrow('Error', ['To be implemented!']), JsLang.astReturn(true)],
    [XemlTypes.Modifier.PROCESSOR]: (args) => [
        JsLang.astThrow('Error', ['To be implemented!']),
        JsLang.astReturn(JsLang.astId(args[0])),
    ],
    [XemlTypes.Modifier.ACTIVATOR]: () => [
        JsLang.astThrow('Error', ['To be implemented!']),
        JsLang.astReturn(JsLang.astId('undefined')),
    ],
};

/**
 * Geml database access object (DAO) modeler.
 * @class
 */
class DaoModeler {
    /**
     * @param {object} modelService
     * @param {XemlLinker} linker - Xeml linker
     * @param {Connector} connector
     */
    constructor(modelService, linker, connector) {
        this.modelService = modelService;
        this.linker = linker;
        this.outputPath = modelService.config.modelPath;
        this.manifestPath = modelService.config.manifestPath;

        this.connector = connector;
    }

    modeling_(schema, versionInfo) {
        this.linker.log('info', 'Generating entity models for schema "' + schema.name + '"...');

        this._generateSchemaModel(schema, versionInfo);
        this._generateEntityModel(schema, versionInfo);
        this._generateEnumTypes(schema, versionInfo);
        this._generateEntityInputSchema(schema, versionInfo);
        this._generateEntityViews(schema);
        //
        //this._generateViewModel();

        if (this.manifestPath) {
            this._generateEntityManifest(schema);
        }
    }

    _featureReducer(schema, entity, featureName, feature) {
        let field;

        switch (featureName) {
            case "autoId":                
                break;

            case "createTimestamp":                
                break;

            case "updateTimestamp":
                break;

            case "userEditTracking":
                break;

            case "logicalDeletion":
                break;

            case "atLeastOneNotNull":
                break;

            case "validateAllFieldsOnCreation":
                break;

            case "stateTracking":
                break;

            case "i18n":
                break;

            case "changeLog":
                break;

            case 'createBefore':      
                field = entity.fields[feature.relation];
                if (field) {
                    field.fillByRule = true;
                }
                break;

            case 'createAfter':                
                break;    

            case 'closureTable':                
                break;

            default:
                throw new Error('Unsupported feature "' + featureName + '".');
        }
    }

    _generateSchemaModel(schema, versionInfo) {
        let capitalized = naming.pascalCase(schema.name);

        let locals = {
            schemaVersion: versionInfo.version,
            driver: this.connector.driver,
            className: capitalized,
            schemaName: schema.name,
            
            entities: Object.keys(schema.entities).map((name) => naming.pascalCase(name)),
        };

        let classTemplate = path.resolve(__dirname, 'database', this.connector.driver, 'Database.js.swig');
        let classCode = swig.renderFile(classTemplate, locals);

        let modelFilePath = path.resolve(this.outputPath, capitalized + '.js');
        fs.ensureFileSync(modelFilePath);
        fs.writeFileSync(modelFilePath, classCode);

        this.linker.log('info', 'Generated database model: ' + modelFilePath);
    }

    _generateEnumTypes(schema, versionInfo) {
        //build types defined outside of entity
        _.forOwn(schema.types, (location, type) => {            
            const typeInfo = schema.linker.getTypeInfo(type, location);
            if (typeInfo.enum && Array.isArray(typeInfo.enum)) {
                const capitalized = naming.pascalCase(type);

                const content = `export default {
    ${typeInfo.enum
        .map((val) => `${naming.snakeCase(val).toUpperCase()}: '${val}'`)
        .join(',\n    ')}                    
};`;

                const modelFilePath = path.resolve(this.outputPath, schema.name, 'types', capitalized + '.js');
                fs.ensureFileSync(modelFilePath);
                fs.writeFileSync(modelFilePath, content);

                this.linker.log('info', 'Generated enum type definition: ' + modelFilePath);
            }
        });
    }

    _generateEntityModel(schema, versionInfo) {
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            if (entity.features) {
                _.forOwn(entity.features, (f, featureName) => {
                    if (Array.isArray(f)) {
                        f.forEach((ff) => this._featureReducer(schema, entity, featureName, ff));
                    } else {
                        this._featureReducer(schema, entity, featureName, f);
                    }
                });
            }

            let capitalized = naming.pascalCase(entityInstanceName);

            //shared information with model CRUD and customized interfaces
            let sharedContext = {
                mapOfFunctorToFile: {},
                newFunctorFiles: [],
            };

            let { ast: astClassMain, fieldReferences } = this._processFieldModifiers(entity, sharedContext);
            astClassMain = [astClassMain];

            //prepare meta data
            let uniqueKeys = [_.castArray(entity.key)];

            if (entity.indexes) {
                entity.indexes.forEach((index) => {
                    if (index.unique) {
                        uniqueKeys.push(index.fields);
                    }
                });
            }

            let modelMeta = {
                schemaName: schema.name,
                name: entityInstanceName,
                keyField: entity.key,
                fields: _.mapValues(entity.fields, (f) => _.omit(f.toJSON(), 'modifiers')),
                features: entity.features || {},
                uniqueKeys,
            };

            if (entity.baseClasses) {
                modelMeta.baseClasses = entity.baseClasses;
            }

            if (!isEmpty(entity.indexes)) {
                modelMeta.indexes = entity.indexes;
            }

            if (!isEmpty(entity.features)) {
                modelMeta.features = entity.features;
            }

            if (!isEmpty(entity.associations)) {
                modelMeta.associations = entity.associations;
            }

            if (!isEmpty(fieldReferences)) {
                modelMeta.fieldDependencies = fieldReferences;
            }

            sharedContext.newFunctorFiles?.forEach((functor) => {
                if (functor.functorType === XemlTypes.Modifier.PROCESSOR) {
                    astClassMain.push(Snippets.processorMethod(functor));
                }
            });

            //build customized interfaces
            if (entity.interfaces) {
                let astInterfaces = this._buildInterfaces(entity, modelMeta, sharedContext);
                //console.log(astInterfaces);
                //let astClass = astClassMain[astClassMain.length - 1];
                //JsLang.astPushInBody(astClass, astInterfaces);
                astClassMain = astClassMain.concat(astInterfaces);
            }

            const importLines = [];
            const assignLines = [];
            const importBucket = {};

            //generate functors if any
            if (!isEmpty(sharedContext.mapOfFunctorToFile)) {
                _.forOwn(sharedContext.mapOfFunctorToFile, (fileName, functionName) => {
                    if (isPlainObject(fileName)) {
                        const importName = fileName.type + 's';
                        const asLocalName= naming.camelCase(fileName.packageName) + importName;
                        pushIntoBucket(importBucket, fileName.packageName, importName); 
                        assignLines.push(JsLang.astToCode(JsLang.astVarDeclare(fileName.functorId, JsLang.astVarRef(asLocalName + '.' + fileName.functionName, false), true)));
                    } else {
                        importLines.push(JsLang.astToCode(JsLang.astImport(functionName, baseName(fileName, true))));
                    }
                });

                _.forOwn(importBucket, (importNames, packageName) => {
                    const names = _.uniq(importNames);
                    names.forEach((importName) => {
                        const asLocalName= naming.camelCase(packageName) + importName;
                        importLines.push(JsLang.astToCode(JsLang.astImportNonDefault(packageName, { name: importName, local: asLocalName })));
                    });
                });
            }

            if (!isEmpty(sharedContext.newFunctorFiles)) {
                _.each(sharedContext.newFunctorFiles, (entry) => {
                    this._generateFunctionTemplateFile(schema, entry, versionInfo);
                });
            }

            //add package path
            const packageName = entity.xemlModule.packageName;
            if (packageName) {
                modelMeta.fromPackage = packageName;
                modelMeta.packagePath = this.modelService.config.dependencies[packageName]; //path.relative(this.linker.dependencies[packageName], this.linker.app.workingPath);
            }

            let locals = {
                schemaVersion: versionInfo.version,
                driver: this.connector.driver,
                imports: importLines.join('\n'),
                assigns: assignLines.join('\n'),
                className: capitalized,
                entityMeta: indentLines(JSON.stringify(modelMeta, null, 4), 4),
                classBody: indentLines(astClassMain.map((block) => JsLang.astToCode(block)).join('\n\n'), 8),
                //mixins
            };

            let classTemplate = path.resolve(__dirname, 'database', this.connector.driver, 'EntityModel.js.swig');
            let classCode = swig.renderFile(classTemplate, locals);

            let modelFilePath = path.resolve(this.outputPath, schema.name, capitalized + '.js');
            fs.ensureFileSync(modelFilePath);
            fs.writeFileSync(modelFilePath, classCode);

            this.linker.log('info', 'Generated entity model: ' + modelFilePath);
        });
    }

    _generateEntityInputSchema(schema, versionInfo) {
        //generate validator config
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            _.each(entity.inputs, (inputs, inputSetName) => {
                const validationSchema = {};
                const dependencies = new Set();
                const ast = JsLang.astProgram(true);

                inputs.forEach((input) => {
                    //:address
                    if (input.name.startsWith(':')) {
                        const assoc = input.name.substr(1);
                        const assocMeta = entity.associations[assoc];

                        if (!assocMeta) {
                            throw new Error(`Association "${assoc}" not found in entity [${entityInstanceName}].`);
                        }

                        if (!input.spec) {
                            throw new Error(
                                `Input "spec" is required for entity reference. Input set: ${inputSetName}, entity: ${entityInstanceName}, local: ${assoc}, referencedEntity: ${assocMeta.entity}`
                            );
                        }

                        const dep = `${assocMeta.entity}-${input.spec}`;
                        dependencies.add(dep);

                        if (assocMeta.list) {
                            validationSchema[input.name] = JsLang.astValue({
                                type: 'array',
                                elementSchema: {
                                    type: 'object',
                                    schema: JsLang.astCall(_.camelCase(dep), []),
                                },
                                ..._.pick(input, ['optional']),
                            });
                        } else {
                            validationSchema[input.name] = JsLang.astValue({
                                type: 'object',
                                schema: JsLang.astCall(_.camelCase(dep), []),
                                ..._.pick(input, ['optional']),
                            });
                        }
                    } else {
                        const field = entity.fields[input.name];

                        if (!field) {
                            throw new Error(`Field "${input.name}" not found in entity [${entityInstanceName}].`);
                        }

                        validationSchema[input.name] = JsLang.astValue({
                            ..._.pick(field, ['type', 'values']),
                            ..._.pick(input, ['optional']),
                        });
                    }
                });

                //console.dir(JsLang.astValue(validationSchema), {depth: 20});

                const exportBody = Array.from(dependencies).map((dep) =>
                    JsLang.astImport(_.camelCase(dep), `./${dep}`)
                );

                JsLang.astPushInBody(
                    ast,
                    JsLang.astAssign(
                        JsLang.astVarRef('module.exports'),
                        JsLang.astAnonymousFunction([], exportBody.concat(JsLang.astReturn(validationSchema)))
                    )
                );

                let inputSchemaFilePath = path.resolve(
                    this.outputPath,
                    schema.name,
                    'inputs',
                    entityInstanceName + '-' + inputSetName + '.js'
                );
                fs.ensureFileSync(inputSchemaFilePath);
                fs.writeFileSync(inputSchemaFilePath, JsLang.astToCode(ast));

                this.linker.log('info', 'Generated entity input schema: ' + inputSchemaFilePath);
            });
        });
    }

    _generateEntityViews(schema) {
        //generate views config
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            if (!isEmpty(entity.views)) {
                const views = _.mapValues(entity.views, viewSet => {
                    return {
                        ...viewSet,
                        $select: viewSet.$select.reduce((columns, field) => {
                            if (isPlainObject(field)) {
                                if (field.$xt === 'ExclusiveSelect') {
                                    const { columnSet, excludes } = field;
                                    let refEntity = entity;

                                    if (columnSet.indexOf('.') !== -1) {
                                        // association
                                        const baseAssoc = extractReferenceBaseName(columnSet);
                                        refEntity = entity.getReferencedEntityByPath(baseAssoc);
                                    } 

                                    // get all fields
                                    for (const fieldName in refEntity.fields) {
                                        if (!excludes.includes("-" + fieldName)) {
                                            columns.push(fieldName);
                                        }
                                    }                                    

                                    return columns;
                                }
                            }

                            columns.push(field);
                            return columns;
                        }, [])
                    }
                });

                let inputSchemaFilePath = path.resolve(
                    this.outputPath,
                    schema.name,
                    'views',
                    entityInstanceName + '.json'
                );
                fs.ensureFileSync(inputSchemaFilePath);
                fs.writeFileSync(inputSchemaFilePath, JSON.stringify(views, null, 4));

                this.linker.log('info', 'Generated entity views data set: ' + inputSchemaFilePath);
            }
        });
    }

    _generateEntityManifest(schema) {
        /*
        let manifest = {};

        _.each(schema.entities, (entity, entityName) => {
            if (entity.info.restful) {
                _.each(entity.info.restful, ({ type, methods }, relativeUri) => {                    
                    let apiInfo = {
                        type,
                        methods: {}                                            
                    };

                    if (type === 'entity') {
                        apiInfo.entity = entityName;
                        apiInfo.displayName = entity.displayName;

                        if (entity.comment) {
                            apiInfo.description = entity.comment;
                        }
                    }

                    _.each(methods, (meta, methodName) => {

                        switch (methodName) {
                            case 'create':
                                apiInfo.methods['post:' + relativeUri] = meta;
                            break;

                            case 'findOne':
                            break;

                            case 'fineAll':
                            break;

                            case 'updateOne':
                            break;

                            case 'updateMany':
                            break;

                            case 'deleteOne':
                            break;

                            case 'deleteMany':
                            break;
                        }

                    });
                });
            }
        });
        */

        /*
        let outputFilePath = path.resolve(this.manifestPath, schema.name + '.manifest.json');
        fs.ensureFileSync(outputFilePath);
        fs.writeFileSync(outputFilePath, JSON.stringify(entities, null, 4));

        this.linker.log('info', 'Generated schema manifest: ' + outputFilePath);
        */

        const diagram = {};

        //generate validator config
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            /*
            let validationSchema = {};

            _.forOwn(entity.fields, (field, fieldName) => {
                if (field.readOnly) return;

                let fieldSchema = {
                    type: field.type,
                };

                if (field.type === "enum") {
                    fieldSchema.values = field.values;
                }

                if (field.optional) {
                    fieldSchema.optional = true;
                }

                validationSchema[fieldName] = fieldSchema;
            });
            */

            diagram[entityInstanceName] = entity.toJSON();

            /*
            let entityOutputFilePath = path.resolve(
                this.manifestPath,
                schema.name,
                "validation",
                entityInstanceName + ".manifest.json"
            );
            fs.ensureFileSync(entityOutputFilePath);
            fs.writeFileSync(entityOutputFilePath, JSON.stringify(validationSchema, null, 4));

            this.linker.log("info", "Generated entity manifest: " + entityOutputFilePath);
            */
        });

        let diagramOutputFilePath = path.resolve(this.manifestPath, schema.name, 'diagram.json');
        fs.ensureFileSync(diagramOutputFilePath);
        fs.writeFileSync(diagramOutputFilePath, JSON.stringify(diagram, null, 4));

        this.linker.log('info', 'Generated schema manifest: ' + diagramOutputFilePath);
    }

    /*
    _generateViewModel(schema, dbService) {        
        _.forOwn(schema.views, (viewInfo, viewName) => {
            this.linker.info('Building view: ' + viewName);

            let capitalized = _.upperFirst(viewName);

            let ast = JsLang.astProgram();

            JsLang.astPushInBody(ast, JsLang.astRequire('Mowa', 'mowa'));
            JsLang.astPushInBody(ast, JsLang.astVarDeclare('Util', JsLang.astVarRef('Mowa.Util'), true));
            JsLang.astPushInBody(ast, JsLang.astVarDeclare('_', JsLang.astVarRef('Util._'), true));
            JsLang.astPushInBody(ast, JsLang.astRequire('View', 'mowa/lib/oolong/runtime/view'));

            let compileContext = OolToAst.createCompileContext(viewName, dbService.serviceId, this.linker);

            compileContext.modelVars.add(viewInfo.entity);

            let paramMeta;

            if (viewInfo.params) {
                paramMeta = this._processParams(viewInfo.params, compileContext);
            }

            let viewMeta = {
                isList: viewInfo.isList,
                params: paramMeta
            };

            let viewBodyTopoId = OolToAst.createTopoId(compileContext, '$view');
            OolToAst.dependsOn(compileContext, compileContext.mainStartId, viewBodyTopoId);

            let viewModeler = require(path.resolve(__dirname, './dao/view', dbService.dbType + '.js'));
            compileContext.astMap[viewBodyTopoId] = viewModeler(dbService, viewName, viewInfo);
            OolToAst.addCodeBlock(compileContext, viewBodyTopoId, {
                type: OolToAst.AST_BLK_VIEW_OPERATION
            });

            let returnTopoId = OolToAst.createTopoId(compileContext, '$return:value');
            OolToAst.dependsOn(compileContext, viewBodyTopoId, returnTopoId);
            OolToAst.compileReturn(returnTopoId, {
                "$xt": "ObjectReference",
                "name": "viewData"
            }, compileContext);

            let deps = compileContext.topoSort.sort();
            this.linker.verbose('All dependencies:\n' + JSON.stringify(deps, null, 2));

            deps = deps.filter(dep => compileContext.mapOfTokenToMeta.has(dep));
            this.linker.verbose('All necessary source code:\n' + JSON.stringify(deps, null, 2));

            let astDoLoadMain = [
                JsLang.astVarDeclare('$meta', JsLang.astVarRef('this.meta'), true, false, 'Retrieving the meta data')
            ];

            _.each(deps, dep => {
                let astMeta = compileContext.mapOfTokenToMeta.get(dep);

                let astBlock = compileContext.astMap[dep];
                assert: astBlock, 'Empty ast block';

                if (astMeta.type === 'ModifierCall') {
                    let fieldName = getFieldName(astMeta.target);
                    let astCache = JsLang.astAssign(JsLang.astVarRef(astMeta.target), astBlock, `Modifying ${fieldName}`);
                    astDoLoadMain.push(astCache);
                    return;
                }

                astDoLoadMain = astDoLoadMain.concat(_.castArray(compileContext.astMap[dep]));
            });

            if (!isEmpty(compileContext.mapOfFunctorToFile)) {
                _.forOwn(compileContext.mapOfFunctorToFile, (fileName, functionName) => {
                    JsLang.astPushInBody(ast, JsLang.astRequire(functionName, '.' + fileName));
                });
            }

            if (!isEmpty(compileContext.newFunctorFiles)) {
                _.each(compileContext.newFunctorFiles, entry => {
                    this._generateFunctionTemplateFile(dbService, entry);
                });
            }

            JsLang.astPushInBody(ast, JsLang.astClassDeclare(capitalized, 'View', [
                JsLang.astMemberMethod('_doLoad', Object.keys(paramMeta),
                    astDoLoadMain,
                    false, true, false, 'Populate view data'
                )
            ], `${capitalized} view`));
            JsLang.astPushInBody(ast, JsLang.astAssign(capitalized + '.meta', JsLang.astValue(viewMeta)));
            JsLang.astPushInBody(ast, JsLang.astAssign('module.exports', JsLang.astVarRef(capitalized)));

            let modelFilePath = path.resolve(this.outputPath, dbService.dbType, dbService.name, 'views', viewName + '.js');
            fs.ensureFileSync(modelFilePath);
            fs.writeFileSync(modelFilePath + '.json', JSON.stringify(ast, null, 2));

            DaoModeler._exportSourceCode(ast, modelFilePath);

            this.linker.log('info', 'Generated view model: ' + modelFilePath);
        });
    };
    */

    /**
     * Process field modifiers and generate ast and field references
     * @param {*} entity 
     * @param {object} sharedContext 
     * @returns {object} { ast, fieldReferences }
     */
    _processFieldModifiers(entity, sharedContext) {
        let compileContext = XemlToAst.createCompileContext(entity.xemlModule.name, entity.xemlModule, this.linker, sharedContext);
        compileContext.variables['raw'] = { source: 'context', finalized: true };
        compileContext.variables['i18n'] = { source: 'context', finalized: true };
        compileContext.variables['connector'] = { source: 'context', finalized: true };
        compileContext.variables['latest'] = { source: 'context' };

        const allFinished = XemlToAst.createTopoId(compileContext, 'done.');

        //map of field name to dependencies
        let fieldReferences = {};

        _.forOwn(entity.fields, (field, fieldName) => {
            let topoId = XemlToAst.compileField(fieldName, field, compileContext);
            XemlToAst.dependsOn(compileContext, topoId, allFinished);

            /* remove self dependency
            if (field.writeOnce || field.freezeAfterNonDefault) {
                pushIntoBucket(fieldReferences, fieldName, { reference: fieldName, writeProtect: true });
            }
            */
        });

        let deps = compileContext.topoSort.sort();
        //this.linker.verbose('All dependencies:\n' + JSON.stringify(deps, null, 2));

        deps = deps.filter((dep) => compileContext.mapOfTokenToMeta.has(dep));
        //this.linker.verbose('All necessary source code:\n' + JSON.stringify(deps, null, 2));

        let methodBodyValidateAndFill = [],
            lastFieldsGroup,
            methodBodyCache = [],
            lastBlock,
            lastAstType; //, hasValidator = false;

        const _mergeDoValidateAndFillCode = function (fieldName, references, astCache, requireTargetField) {
            let fields = [fieldName].concat(references);
            let checker = fields.join(',');

            if (lastFieldsGroup && lastFieldsGroup.checker !== checker) {
                methodBodyValidateAndFill = methodBodyValidateAndFill.concat(
                    Snippets._fieldRequirementCheck(
                        lastFieldsGroup.fieldName,
                        lastFieldsGroup.references,
                        methodBodyCache,
                        lastFieldsGroup.requireTargetField
                    )
                );
                methodBodyCache = [];
            }

            methodBodyCache = methodBodyCache.concat(astCache);
            lastFieldsGroup = {
                fieldName,
                references,
                requireTargetField,
                checker,
            };
        };

        //console.dir(compileContext.astMap['mobile~isMobilePhone:arg[1]|>stringDasherize'], { depth: 8 });

        _.each(deps, (dep, i) => {
            //get metadata of source code block
            let sourceMap = compileContext.mapOfTokenToMeta.get(dep);

            //get source code block
            let astBlock = compileContext.astMap[dep];

            let targetFieldName = getFieldName(sourceMap.target);

            if (sourceMap.references && sourceMap.references.length > 0) {
                let fieldReference = fieldReferences[targetFieldName];
                if (!fieldReference) {
                    fieldReferences[targetFieldName] = fieldReference = [];
                }

                if (sourceMap.type === XemlToAst.AST_BLK_ACTIVATOR_CALL) {
                    sourceMap.references.forEach((ref) => {
                        fieldReference.push({ reference: ref, whenNull: true });
                    });
                } else {
                    sourceMap.references.forEach((ref) => {
                        if (fieldReference.indexOf(ref) === -1) fieldReference.push(ref);
                    });
                }
            }

            if (lastBlock) {
                astBlock = chainCall(lastBlock, lastAstType, astBlock, sourceMap.type);
                lastBlock = undefined;
            }

            if (i < deps.length - 1) {
                let nextType = compileContext.mapOfTokenToMeta.get(deps[i + 1]);

                if (isChainable(sourceMap, nextType)) {
                    lastBlock = astBlock;
                    lastAstType = sourceMap.type;
                    return;
                }
            }

            if (sourceMap.type === XemlToAst.AST_BLK_VALIDATOR_CALL) {
                //hasValidator = true;
                let astCache = Snippets._validateCheck(targetFieldName, astBlock);
                _mergeDoValidateAndFillCode(targetFieldName, sourceMap.references, astCache, true);
            } else if (sourceMap.type === XemlToAst.AST_BLK_PROCESSOR_CALL) {
                let astCache = JsLang.astAssign(
                    JsLang.astVarRef(sourceMap.target, true),
                    astBlock,
                    `Processing "${targetFieldName}"`
                );

                _mergeDoValidateAndFillCode(targetFieldName, sourceMap.references, astCache, true);
            } else if (sourceMap.type === XemlToAst.AST_BLK_ACTIVATOR_CALL) {
                let astCache = Snippets._checkAndAssign(
                    astBlock,
                    JsLang.astVarRef(sourceMap.target, true),
                    `Activating "${targetFieldName}"`
                );

                _mergeDoValidateAndFillCode(targetFieldName, sourceMap.references, astCache, false);
            } else {
                throw new Error('To be implemented.');
                //astBlock = _.castArray(astBlock);
                //_mergeDoValidateAndFillCode(targetFieldName, [], astBlock);
            }
        });

        /* Changed to throw error instead of returning a error object
        if (hasValidator) {
            let declare = JsLang.astVarDeclare(validStateName, false);
            methodBodyCreate.unshift(declare);
            methodBodyUpdate.unshift(declare);
        }
        */

        if (!isEmpty(methodBodyCache)) {
            methodBodyValidateAndFill = methodBodyValidateAndFill.concat(
                Snippets._fieldRequirementCheck(
                    lastFieldsGroup.fieldName,
                    lastFieldsGroup.references,
                    methodBodyCache,
                    lastFieldsGroup.requireTargetField
                )
            );
        }

        /*
        let ast = JsLang.astProgram();
        JsLang.astPushInBody(ast, JsLang.astClassDeclare('Abc', 'Model', [
            JsLang.astMemberMethod(asyncMethodNaming('prepareEntityData_'), [ 'context' ],
            Snippets._doValidateAndFillHeader.concat(methodBodyValidateAndFill).concat([ JsLang.astReturn(JsLang.astId('context')) ]),
            false, true, true
        )], 'comment'));
        */

        return {
            ast: JsLang.astMemberMethod(
                asyncMethodNaming('applyModifiers'),
                ['context', 'isUpdating'],
                Snippets._applyModifiersHeader
                    .concat(methodBodyValidateAndFill)
                    .concat([JsLang.astReturn(JsLang.astId('context'))]),
                false,
                true,
                false,
                'Applying predefined modifiers to entity fields.'
            ),
            fieldReferences,
        };
    }

    _generateFunctionTemplateFile(schema, { functionName, functorType, fileName, args }, versionInfo) {
        let filePath = path.resolve(this.outputPath, schema.name, fileName);

        let ast;

        if (fs.existsSync(filePath)) {
            ast = esprima.parseModule(fs.readFileSync(filePath, 'utf8'), { tokens: true, comment: true });
            ast.body[0].leadingComments = JsLang.astLeadingComments(` v.${versionInfo.version} by xeml`).leadingComments;

            fs.ensureFileSync(filePath);
            fs.writeFileSync(filePath, JsLang.astToCode(ast));

            this.linker.log('warn', `${_.upperFirst(functorType)} "${fileName}" exists.`);
        } else {
            ast = JsLang.astProgram(true);            

            JsLang.astPushInBody(ast, JsLang.astFunction(functionName, args, XEML_MODIFIER_RETURN[functorType](args)));
            JsLang.astPushInBody(ast, JsLang.astExportDefault(functionName));
            ast.body[0].leadingComments = JsLang.astLeadingComments(` v.${versionInfo.version} by xeml`).leadingComments;
        }        

        fs.ensureFileSync(filePath);
        fs.writeFileSync(filePath, JsLang.astToCode(ast));
        this.linker.log('info', `Generated ${functorType} file: ${filePath}`);
    }

    _buildInterfaces(entity, modelMetaInit, sharedContext) {
        let ast = [];

        _.forOwn(entity.interfaces, (method, name) => {
            this.linker.info('Building interface: ' + name);

            let astBody = [
                JsLang.astVarDeclare(
                    '$meta',
                    JsLang.astVarRef('this.meta.interfaces.' + name),
                    true,
                    false,
                    'Retrieving the meta data'
                ),
            ];

            let compileContext = XemlToAst.createCompileContext(entity.xemlModule.name, entity.xemlModule, this.linker, sharedContext);

            let paramMeta;

            if (method.accept) {
                paramMeta = this._processParams(method.accept, compileContext);
            }

            //metadata
            modelMetaInit['interfaces'] || (modelMetaInit['interfaces'] = {});
            modelMetaInit['interfaces'][name] = { params: Object.values(paramMeta) };

            _.each(method.implementation, (operation, index) => {
                //let lastTopoId =
                XemlToAst.compileDbOperation(index, operation, compileContext, compileContext.mainStartId);
            });

            if (method.return) {
                XemlToAst.compileExceptionalReturn(method.return, compileContext);
            }

            let deps = compileContext.topoSort.sort();
            //this.linker.verbose('All dependencies:\n' + JSON.stringify(deps, null, 2));

            deps = deps.filter((dep) => compileContext.mapOfTokenToMeta.has(dep));
            //this.linker.verbose('All necessary source code:\n' + JSON.stringify(deps, null, 2));

            _.each(deps, (dep) => {
                let sourceMap = compileContext.mapOfTokenToMeta.get(dep);
                let astBlock = compileContext.astMap[dep];

                //this.linker.verbose('Code point "' + dep + '":\n' + JSON.stringify(sourceMap, null, 2));

                let targetFieldName = sourceMap.target; //getFieldName(sourceMap.target);

                if (sourceMap.type === XemlToAst.AST_BLK_VALIDATOR_CALL) {
                    astBlock = Snippets._validateCheck(targetFieldName, astBlock);
                } else if (sourceMap.type === XemlToAst.AST_BLK_PROCESSOR_CALL) {
                    if (sourceMap.needDeclare) {
                        astBlock = JsLang.astVarDeclare(
                            JsLang.astVarRef(sourceMap.target),
                            astBlock,
                            false,
                            false,
                            `Processing "${targetFieldName}"`
                        );
                    } else {
                        astBlock = JsLang.astAssign(
                            JsLang.astVarRef(sourceMap.target, true),
                            astBlock,
                            `Processing "${targetFieldName}"`
                        );
                    }
                } else if (sourceMap.type === XemlToAst.AST_BLK_ACTIVATOR_CALL) {
                    if (sourceMap.needDeclare) {
                        astBlock = JsLang.astVarDeclare(
                            JsLang.astVarRef(sourceMap.target),
                            astBlock,
                            false,
                            false,
                            `Processing "${targetFieldName}"`
                        );
                    } else {
                        astBlock = JsLang.astAssign(
                            JsLang.astVarRef(sourceMap.target, true),
                            astBlock,
                            `Activating "${targetFieldName}"`
                        );
                    }
                }

                astBody = astBody.concat(_.castArray(astBlock));
            });

            ast.push(
                JsLang.astMemberMethod(
                    asyncMethodNaming(name),
                    Object.keys(paramMeta),
                    astBody,
                    false,
                    true,
                    true,
                    text.replaceAll(_.kebabCase(name), '-', ' ')
                )
            );
        });

        return ast;
    }

    _processParams(acceptParams, compileContext) {
        let paramMeta = {};

        acceptParams.forEach((param, i) => {
            XemlToAst.compileParam(i, param, compileContext);
            paramMeta[param.name] = param;
            compileContext.variables[param.name] = { source: 'argument' };
        });

        return paramMeta;
    }
}

module.exports = DaoModeler;