modeler/Dao.js

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

const XemlTypes = require('../lang/XemlTypes');
const Field = require('../lang/Field');
const JsLang = require('./util/ast');
const XemlToAst = require('./util/xemlToAst');
const Snippets = require('./dao/snippets');
const Methods = require('./dao/methods');
const { extractReferenceBaseName, isDotSeparateName } = require('../lang/XemlUtils');
const { globSync } = require('glob');
const yaml = require('yaml');

const INPUT_SCHEMA_DERIVED_KEYS = [
    'type',
    'noTrim',
    'emptyAsNull',
    'encoding',
    'format',
    'schema',
    'element',
    'keepUnsanitized',
    'valueSchema',
    'delimiter',
    'csv',
    'enum',
];
const DATASET_FIELD_KEYS = [...INPUT_SCHEMA_DERIVED_KEYS, 'optional', 'default'];

const MAP_META_TO_MODIFIER = {
    fixedLength: 'length',
    maxLength: 'maxLength',
    minLength: 'minLength',
};

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.connector = connector;
    }

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

        this._generateDbModel(schema, versionInfo);
        const datasetEntries = this._generateEntityDatasetSchema(schema, versionInfo);
        const { sharedModifiers } = this._generateEntityModel(schema, datasetEntries, versionInfo);
        this._generateSharedModifiers(schema, sharedModifiers, versionInfo);
        this._generateEnumTypes(schema, versionInfo);
        //this._generateEntityInputSchema(schema, versionInfo);
        this._generateEntityViews(schema);
        //this.generateApi(schema, versionInfo);
        //
        //this._generateViewModel();
    }

    buildApiClient(schema, versionInfo) {
        this.linker.log('info', 'Generating API client for schema "' + schema.name + '"...');

        const schemaPath = path.join(path.dirname(schema.linker.getModulePathById(schema.xemlModule.id)), 'api');

        // check if api folder exists and read all yaml files under api folder
        const datasetFiles = globSync('*.yaml', { nodir: true, cwd: schemaPath });
        const datasetFilesSet = new Set(datasetFiles);

        if (datasetFilesSet.size === 0) {
            return;
        }

        const context = {
            schema,
            versionInfo,
            sharedTypes: {},
        };

        const typeSpecialize = (typeInfo, argsMap) => {};

        // process __types.yaml for type definitions
        if (datasetFilesSet.has('__types.yaml')) {
            const types = yaml.parse(fs.readFileSync(path.join(schemaPath, '__types.yaml'), 'utf8'));
            // check if any type has $args property, change the type to a function

            _.each(types, (type, typeName) => {
                let typeInfo = type;

                if (type.$args) {
                    typeInfo = (variables) => {
                        if (type.$args.find((arg) => !(arg in variables))) {
                            throw new Error(
                                `Specialization argument "${arg}" is required for type constructor "${typeName}".`
                            );
                        }
                        return typeSpecialize(type, variables);
                    };
                }

                context.sharedTypes[typeName] = typeInfo;
            });
            datasetFilesSet.delete('__types.yaml');
        }

        // process __groups.yaml for group definitions
        if (datasetFilesSet.has('__groups.yaml')) {
            context.groups = yaml.parse(fs.readFileSync(path.join(schemaPath, '__groups.yaml'), 'utf8'));
            datasetFilesSet.delete('__groups.yaml');
        }

        for (const datasetFile of datasetFilesSet) {
            if (datasetFile.startsWith('_')) {
                continue;
            }

            const resourceName = path.basename(datasetFile, '.yaml');
            const resources = yaml.parse(fs.readFileSync(path.join(schemaPath, datasetFile), 'utf8'));
            _.each(resources, (resourceInfo, baseEndpoint) => {
                this._generateResourceApi(context, resourceName, baseEndpoint, resourceInfo);
            });
        }
    }

    _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 '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 'hasClosureTable':
                return [
                    Methods.getAllDescendants(entity, feature),
                    Methods.getAllAncestors(entity, feature),
                    Methods.addChildNode(feature.relation, feature.closureTable),
                    Methods.removeSubTree(feature.relation, feature.closureTable),
                    Methods.cloneSubTree(entity, feature),
                    Methods.getTopNodes(entity, feature.closureTable),
                    Methods.moveNode(entity),
                    Methods.getChildren(feature.reverse),
                    Methods.getParents(feature.relation),
                ];

            case 'isCacheTable':
                break;

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

        return [];
    }

    _generateDbModel(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.join(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, (typeInfo, type) => {
            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.join(this.outputPath, schema.name, 'types', capitalized + '.js');
                fs.ensureFileSync(modelFilePath);
                fs.writeFileSync(modelFilePath, content);

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

    _generateSharedModifiers(schema, sharedModifiers, versionInfo) {
        _.each(sharedModifiers, (modifier) => {
            this._generateFunctionTemplateFile(schema, modifier, versionInfo);
        });
    }

    _generateEntityModel(schema, _datasetFiles, versionInfo) {
        const sharedModifiers = {};

        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            const extraMethods = [];
            const datasetFiles = _datasetFiles[entityInstanceName];

            if (entity.features) {
                _.forOwn(entity.features, (f, featureName) => {
                    if (Array.isArray(f)) {
                        f.forEach((ff) => extraMethods.push(...this._featureReducer(schema, entity, featureName, ff)));
                    } else {
                        extraMethods.push(...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 (entity.baseClasses.includes('_messageQueue')) {
                    extraMethods.push(Methods.popJob(this.connector, entityInstanceName));
                    extraMethods.push(Methods.postJob());
                    extraMethods.push(Methods.jobDone());
                    extraMethods.push(Methods.jobFail());
                    extraMethods.push(Methods.retry());
                }

                if (entity.baseClasses.includes('_deferredQueue')) {
                    extraMethods.push(
                        ...[
                            Methods.postDeferredJob(),
                            Methods.removeExpiredJobs(),
                            Methods.getDueJobs(),
                            Methods.getBatchStatus(),
                        ]
                    );
                }
            }

            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));
                }
            });

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

            importLines.push(JsLang.astToCode(Snippets.importFromData()));
            if (datasetFiles) {
                _.each(datasetFiles, (datasetFile, datasetName) => {
                    importLines.push(
                        JsLang.astToCode(
                            JsLang.astImport('schema_' + datasetName, './' + path.join('schema', datasetFile))
                        )
                    );
                });
            }

            if (entity.modifiers) {
                const modifiers = this._processUsedModifiers(entity);

                modifiers.forEach((modifier) => {
                    importLines.push(
                        JsLang.astToCode(
                            JsLang.astImport(
                                dropIfEndsWith(modifier.name, '_') +
                                    modifier.$xt +
                                    (modifier.name.endsWith('_') ? '_' : ''),
                                './' + path.join(modifier.$xt.toLowerCase() + 's', modifier.name)
                            )
                        )
                    );
                    astClassMain.push(Snippets.modifierMethod(modifier));
                });
            }

            if (!isEmpty(datasetFiles)) {
                const datasetSchemas = {};
                _.each(datasetFiles, (v, datasetName) => {
                    const k = 'schema_' + datasetName;
                    datasetSchemas[datasetName] = JsLang.astId(k);
                });

                staticLines.push(
                    JsLang.astToCode(JsLang.astAssign(`${capitalized}.meta.schemas`, JsLang.astValue(datasetSchemas)))
                );
            }

            //generate functors if any
            if (!isEmpty(sharedContext.mapOfFunctorToFile)) {
                _.forOwn(sharedContext.mapOfFunctorToFile, (fileName, functionName) => {
                    if (isPlainObject(fileName)) {
                        const importName = fileName.type + 's';
                        const asLocalName =
                            (fileName.packageName ? naming.camelCase(fileName.packageName) : '') + importName;
                        if (fileName.packageName) {
                            pushIntoBucket(importBucket, fileName.packageName, importName);

                            assignLines.push(
                                JsLang.astToCode(
                                    JsLang.astVarDeclare(
                                        fileName.functorId,
                                        JsLang.astVarRef(asLocalName + '.' + fileName.functionName, false),
                                        true
                                    )
                                )
                            );
                        } else {
                            // same package
                            sharedModifiers[functionName] = fileName;
                            importLines.push(
                                JsLang.astToCode(
                                    JsLang.astImport(fileName.functorId, baseName(fileName.fileName, 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);
                });
            }

            //import views
            if (!isEmpty(entity.views)) {
                importLines.push(JsLang.astToCode(JsLang.astImport('views', './views/' + entity.name + '.json')));

                staticLines.push(
                    JsLang.astToCode(JsLang.astAssign(`${capitalized}.meta.views`, JsLang.astId('views')))
                );
            }

            //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'),
                extraMethods: extraMethods.join('\n'),
                className: capitalized,
                entityMeta: indentLines(JSON.stringify(modelMeta, null, 4), 4),
                classBody: indentLines(astClassMain.map((block) => JsLang.astToCode(block)).join('\n\n'), 8),
                statics: staticLines.join('\n'),
                //mixins
            };

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

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

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

        return { sharedModifiers };
    }

    /*
    _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', 'default']),
                            });
                        } else {
                            validationSchema[input.name] = JsLang.astValue({
                                type: 'object',
                                schema: JsLang.astCall(_.camelCase(dep), []),
                                ..._.pick(input, ['optional', 'default']),
                            });
                        }
                    } 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', 'default']),
                        });
                    }
                });

                //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.join(
                    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);
            });
        });
    }
    */

    _fieldMetaToModifiers(fieldMeta) {
        const result = [];
        for (let key in fieldMeta) {
            const mapped = MAP_META_TO_MODIFIER[key];
            if (mapped) {
                result.push([`~${mapped}`, fieldMeta[key]]);
            }
        }

        return result;
    }

    _processTypeInfo(typeInfo, entity, name, options) {
        if (entity && typeInfo.type && name) {
            let [mixed] = this.linker.trackBackType(entity.xemlModule, typeInfo);
            const field = new Field(name, mixed);
            field.link();
            typeInfo = field;
        }

        const postProcessors = this._fieldMetaToModifiers(typeInfo);
        const extra =
            postProcessors.length > 0
                ? { post: typeInfo?.post ? postProcessors.concat(typeInfo.post) : postProcessors }
                : {};

        return JsLang.astValue({
            ..._.pick(typeInfo, DATASET_FIELD_KEYS),
            ...extra,
            ...options,
        });
    }

    _generateEntityDatasetSchema(schema, versionInfo) {
        const _datasetEntries = {};

        function getReferencedFieldInfo(entity, fieldRef) {
            const [refEntity, name] = splitLast(fieldRef, '.');

            let _entity = entity;

            if (refEntity) {
                _entity = entity.getReferencedEntity(refEntity);
            }

            const field = _entity.fields[name];

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

            return field.toJSON();
        }

        //generate validator config
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            const datasetEntries = {};
            const entityPath = path.dirname(entity.linker.getModulePathById(entity.xemlModule.id));

            // read <entity>-schema-<inputSetName>.yaml
            const prefix = `${entityInstanceName}-schema-`;
            const pattern = `${prefix}*.yaml`;
            const datasetFiles = globSync(pattern, { nodir: true, cwd: entityPath });

            datasetFiles.forEach((datasetFile) => {
                const inputSetName = path.basename(datasetFile, '.yaml').substring(prefix.length);
                const { schema: datasetSchema, ...others } = yaml.parse(
                    fs.readFileSync(path.join(entityPath, datasetFile), 'utf8')
                );

                const _validationSchema = {};
                const dependencies = new Set();
                const ast = JsLang.astProgram(true);

                for (let key in datasetSchema) {
                    let name = key;
                    let _fieldOptions = {};
                    const input = datasetSchema[key];

                    if (name.endsWith('?')) {
                        name = name.slice(0, -1);
                        _fieldOptions.optional = true;
                    }

                    //:address
                    if (name.startsWith('+')) {
                        // extra field that not in the entity
                        name = name.substring(1);
                        _validationSchema[name] = this._processTypeInfo(
                            typeof input === 'string' ? getReferencedFieldInfo(entity, input) : input,
                            entity,
                            name,
                            _fieldOptions
                        );
                    } else if (name.startsWith(':')) {
                        const assoc = name.substring(1);
                        const assocMeta = entity.associations[assoc];

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

                        if (!input.spec) {
                            // link to the dataset of the referenced entity
                            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[name] = JsLang.astValue({
                                type: 'array',
                                element: {
                                    type: 'object',
                                    schema: JsLang.astCall(_.camelCase(dep), []),
                                },
                                ..._.pick(input, DATASET_FIELD_KEYS),
                                ..._fieldOptions,
                            });
                        } else {
                            _validationSchema[name] = JsLang.astValue({
                                type: 'object',
                                schema: JsLang.astCall(_.camelCase(dep), []),
                                ..._.pick(input, DATASET_FIELD_KEYS),
                                ..._fieldOptions,
                            });
                        }
                    } else {
                        const field = entity.fields[name];

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

                        const mixed = { ...field, ...input };
                        const postProcessors = this._fieldMetaToModifiers(mixed);
                        const extra =
                            postProcessors.length > 0
                                ? { post: input?.post ? postProcessors.concat(input.post) : postProcessors }
                                : {};

                        _validationSchema[name] = JsLang.astValue({
                            ..._.pick(field, INPUT_SCHEMA_DERIVED_KEYS),
                            ..._.pick(input, DATASET_FIELD_KEYS),
                            ...extra,
                            ..._fieldOptions,
                        });
                    }
                }

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

                const validationSchema = {
                    schema: _validationSchema,
                    ...others,
                };

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

                JsLang.astPushInBody(
                    ast,
                    JsLang.astFunction('schemaCreator', [], exportBody.concat(JsLang.astReturn(validationSchema)))
                );

                JsLang.astPushInBody(ast, JsLang.astExportDefault('schemaCreator'));

                let inputSchemaFilePath = path.join(
                    this.outputPath,
                    schema.name,
                    'schema',
                    entityInstanceName + '-' + inputSetName + '.js'
                );
                fs.ensureFileSync(inputSchemaFilePath);
                const code = `// v.${versionInfo.version} by xeml\n` + JsLang.astToCode(ast);
                fs.writeFileSync(inputSchemaFilePath, code);
                datasetEntries[inputSetName] = entityInstanceName + '-' + inputSetName;

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

            _datasetEntries[entityInstanceName] = datasetEntries;
        });

        return _datasetEntries;
    }

    _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.join(
                    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);
            }
        });
    }

    prepareApiCommonContext(schemaPath, context) {        
        const apiDefFiles = globSync('*.yaml', { nodir: true, cwd: schemaPath });
        const apiDefFilesSet = new Set(apiDefFiles);

        if (apiDefFilesSet.size === 0) {
            return apiDefFilesSet;
        }

        // todo: typeSpecialize
        const typeSpecialize = (typeInfo, argsMap) => {};

        // process __types.yaml for type definitions
        if (apiDefFilesSet.has('__types.yaml')) {
            const types = yaml.parse(fs.readFileSync(path.join(schemaPath, '__types.yaml'), 'utf8'));
            const sharedTypes = {};

            // check if any type has $args property, change the type to a function

            _.each(types, (type, typeName) => {
                let typeInfo = type;

                if (type.$args) {
                    typeInfo = (variables) => {
                        if (type.$args.find((arg) => !(arg in variables))) {
                            throw new Error(
                                `Specialization argument "${arg}" is required for type constructor "${typeName}".`
                            );
                        }
                        return typeSpecialize(type, variables);
                    };
                }

                sharedTypes[typeName] = typeInfo;
            });

            context.sharedTypes = { ...context.sharedTypes,...sharedTypes };
            apiDefFilesSet.delete('__types.yaml');
        }

        // process __groups.yaml for group definitions
        if (apiDefFilesSet.has('__groups.yaml')) {
            const groups = yaml.parse(fs.readFileSync(path.join(schemaPath, '__groups.yaml'), 'utf8'));
            context.groups = {...context.groups, ...groups};
            apiDefFilesSet.delete('__groups.yaml');
        }

        // process __responses.yaml for response definitions
        if (apiDefFilesSet.has('__responses.yaml')) {
            const responses = yaml.parse(fs.readFileSync(path.join(schemaPath, '__responses.yaml'), 'utf8'));
            context.responses = {...context.responses,...responses};
            apiDefFilesSet.delete('__responses.yaml');
        }

        return apiDefFilesSet;
    }

    generateApi(schema, context) {        
        const schemaPath = path.join(path.dirname(schema.linker.getModulePathById(schema.xemlModule.id)), 'api');
        const apiDefFilesSet = this.prepareApiCommonContext(schemaPath, context);  

        context.schema = schema;

        for (const datasetFile of apiDefFilesSet) {
            if (datasetFile.startsWith('_')) {
                continue;
            }

            const resourceName = path.basename(datasetFile, '.yaml');
            const resources = yaml.parse(fs.readFileSync(path.join(schemaPath, datasetFile), 'utf8'));
            _.each(resources, (resourceInfo, baseEndpoint) => {
                this._generateResourceApi(context, resourceName, baseEndpoint, resourceInfo);
            });
        }

        // generate index file for all groups
        if (context.groups) {
            for (let key in context.groups) {
                const groupInfo = context.groups[key];
                if (groupInfo.moduleSource === 'project') {
                    const groupPath = path.join(this.modelService.config.sourcePath, groupInfo.controllerPath);

                    const apiFiles = globSync('**/*.js', { nodir: true, cwd: groupPath });

                    // filter out index.js and sort the rest and make an export list
                    const exportList = apiFiles
                        .filter((f) => f !== 'index.js')
                        .sort()
                        .map((f) => {
                            const name = baseName(f, true);
                            const localName = replaceAll(name, '/', '__');
                            return `export { default as ${localName} } from './${name}';`;
                        });

                    // override index.js
                    const indexFilePath = path.join(groupPath, 'index.js');
                    fs.writeFileSync(indexFilePath, exportList.join('\n'));
                }
            }
        }
    }

    _generateResourceApi(context, resourceName, baseEndpoint, resourceInfo) {
        if (!baseEndpoint.startsWith('/')) {
            throw new Error("Base endpoint should start with '/'.");
        }

        const resourceClassName = naming.pascalCase(resourceName);
        const { description, group, endpoints } = resourceInfo;

        const groupInfo = context.groups?.[group];

        if (groupInfo == null) {
            throw new Error(`Group "${group}" not found in "xeml/api/__groups.yaml" or extended packages' groups defintion.`);
        }

        const locals = {
            baseEndpoint,
            className: resourceClassName,
            resourceName,
            description,
            methods: [],
        };

        _.each(endpoints, (endpointInfo, endpoint) => {
            if (endpoint.startsWith('/')) {
                const paramName = endpoint.substring(2, endpoint.length - 1);

                // routes with id
                _.each(endpointInfo, (endpointInfo, endpoint) => {
                    this._generateResourceEndpoint(context, locals, endpoint, endpointInfo, paramName);
                });

                return;
            }

            this._generateResourceEndpoint(context, locals, endpoint, endpointInfo);
        });

        locals.methods = locals.methods.join('\n\n');

        const classTemplate = path.resolve(__dirname, 'dao', 'Api.js.swig');
        const classCode = swig.renderFile(classTemplate, locals);

        const resourceFilePath = path.join(
            this.modelService.config.sourcePath,
            groupInfo.controllerPath,
            baseEndpoint
                .split('/')
                .filter((p) => p)
                .map((p) => naming.camelCase(p))
                .join('/') + '.js'
        );
        fs.ensureFileSync(resourceFilePath);
        fs.writeFileSync(resourceFilePath, classCode);

        this.linker.log('info', 'Generated api controller: ' + resourceFilePath);
    }

    _ensureEntity(localContext, entityName, codeBucket) {
        if (!localContext.entities.has(entityName)) {
            codeBucket.push(`const ${naming.pascalCase(entityName)} = this.$m('${entityName}');`);
            localContext.entities.add(entityName);
        }
    }

    _getReferencedMetadata(context, localContext, type, codeBucket) {
        if (type.startsWith('$dataset.')) {
            const [entityName, datasetName] = type.substring(9).split('.');
            this._ensureEntity(localContext, entityName, codeBucket);
            return JsLang.astVarRef(`${naming.pascalCase(entityName)}.datasetSchema('${datasetName}')`);
        }

        if (type.startsWith('$entity.')) {
            const [entityName, fieldName] = type.substring(8).split('.');
            this._ensureEntity(localContext, entityName, codeBucket);
            return JsLang.astVarRef(`${naming.pascalCase(entityName)}.meta.fields['${fieldName}']`);
        }

        if (type.startsWith('$view.')) {
            const [entityName, viewName] = type.substring(6).split('.');
            this._ensureEntity(localContext, entityName, codeBucket);
            return JsLang.astVarRef(`${naming.pascalCase(entityName)}.meta.views['${viewName}']`);
        }

        if (type.startsWith('$type.')) {
            const typeName = type.substring(6);
            return JsLang.astValue(context.sharedTypes[typeName]);
        }

        throw new Error(`Unsupported reference: ${type}`);
    }

    _processPlainObjectMetadata(context, localContext, object, codeBucket) {
        return JsLang.astValue(
            _.mapValues(object, (v) => this._getRequestSourceMetadata(context, localContext, v, codeBucket))
        );
    }

    _getRequestSourceMetadata(context, localContext, typeInfo, codeBucket) {
        if (typeof typeInfo === 'string') {
            return this._getReferencedMetadata(context, localContext, typeInfo, codeBucket);
        }

        const { type, ...others } = typeInfo;

        if (type[0] === '$') {
            return JsLang.astObjectCreate(
                JsLang.astSpread(this._getReferencedMetadata(context, localContext, type, codeBucket)),
                ...JsLang.astValue(others).properties
            );
        }

        return this._processTypeInfo(typeInfo);
    }

    _extractRequestData(context, localContext, localName, collection, source, codeBucket) {
        if (typeof collection === 'object') {
            const { $base, ...others } = collection;
            if ($base) {
                const baseMetadata = this._getReferencedMetadata(context, localContext, $base, codeBucket);
                codeBucket.push(
                    `const ${localName} = Types.OBJECT.sanitize(${source}, ${JsLang.astToCode(baseMetadata)});`
                );

                const metadata = this._processPlainObjectMetadata(context, localContext, others, codeBucket);

                codeBucket.push(
                    `const ${localName}_ = Types.OBJECT.sanitize(${source}, { type: 'object', schema: ${JsLang.astToCode(
                        metadata
                    )} });`,
                    `Object.assign(${localName}, ${localName}_)`
                );

                return;
            }

            const metadata = this._processPlainObjectMetadata(context, localContext, others, codeBucket);
            codeBucket.push(
                `const ${localName} = Types.OBJECT.sanitize(${source}, { type: 'object', schema: ${JsLang.astToCode(
                    metadata
                )} });`
            );
            return;
        }

        const dataCode = this._getReferencedMetadata(context, localContext, collection, codeBucket);
        const typeInfo = JsLang.astToCode(dataCode);
        codeBucket.push(`const ${localName} = Types.OBJECT.sanitize(${source}, ${typeInfo});`);
    }

    _extractTrustVariables(context, localContext, collection, source, codeBucket) {
        if (collection) {
            if (!Array.isArray(collection)) {
                throw new Error(`Variable source "${source}" should be an array of keys.`);
            }

            collection.forEach((key) => {
                const localName = naming.camelCase(replaceAll(key, '.', '-'));

                if (localContext.variables.has(localName)) {
                    throw new Error(`Variable "${localName}" conflicts with local context.`);
                }

                codeBucket.push(`const ${localName} = _.get(${source}, '${key}');`);
            });
        }
    }

    _extractRequestVariables(context, localContext, collection, source, codeBucket) {
        if (collection) {
            _.each(collection, (typeInfo, key) => {
                const localName = naming.camelCase(key);
                const processsedTypeInfo = this._getRequestSourceMetadata(context, localContext, typeInfo, codeBucket);
                let varDef = 'const ';

                if (localContext.variables.has(localName)) {
                    varDef = '';
                }

                if (source === 'ctx.params' && key === localContext.subRouteParam) {
                    codeBucket.push(
                        `${varDef}${localName} = typeSystem.sanitize(${key}, ${JsLang.astToCode(processsedTypeInfo)});`
                    );
                } else {
                    codeBucket.push(
                        `${varDef}${localName} = typeSystem.sanitize(${source}['${key}'], ${JsLang.astToCode(
                            processsedTypeInfo
                        )});`
                    );
                }
            });
        }
    }

    _translateArg(context, localContext, arg) {
        if (arg.startsWith('$local.')) {
            return arg.substring(7);
        }
    }

    _generateCodeLine(context, localContext, line, codeBucket) {
        if (line.startsWith('$business.')) {
            const [business, calling] = splitFirst(line.substring(10), '.');
            if (!localContext.businesses.has(business)) {
                codeBucket.push(`const ${business}Bus = this.app.bus('${business}');`);
            }

            let [method, argsString] = splitFirst(calling, '(');
            const endOfArg = argsString.lastIndexOf(')');

            if (endOfArg === -1) {
                throw new Error('Invalid business invoking syntax: ' + line);
            }

            argsString = argsString.substring(0, endOfArg);
            const args = argsString.split(',').map((a) => this._translateArg(context, localContext, a.trim()));
            const isAsync = method.endsWith('_');
            codeBucket.push(
                `let { result, payload } = ${isAsync ? 'await ' : ''}${business}Bus.${method}(${args.join(', ')});`
            );
        }
    }

    _generateResourceEndpoint(context, locals, method, endpointInfo, subRouteParam) {
        const { description, request, responses, implementation } = endpointInfo;

        const localContext = {
            entities: new Set(),
            businesses: new Set(),
            variables: new Set(),
            subRouteParam,
        };

        if (subRouteParam != null) {
            localContext.variables.add(subRouteParam);
        }

        let codeSanitize = [];

        if (request) {
            const { headers, query, params, body, state, ctx } = request;

            if (body) {
                this._extractRequestData(context, localContext, 'body', body, 'ctx.request.body', codeSanitize);
            }

            if (query) {
                this._extractRequestData(context, localContext, 'query', query, 'ctx.query', codeSanitize);
            }

            this._extractRequestVariables(context, localContext, headers, 'ctx.headers', codeSanitize);
            this._extractRequestVariables(context, localContext, params, 'ctx.params', codeSanitize);
            this._extractTrustVariables(context, localContext, state, 'ctx.state', codeSanitize);
        }

        let codeImplement = [];

        if (implementation) {
            implementation.forEach((line) => {
                this._generateCodeLine(context, localContext, line, codeImplement);
            });
        }

        if (subRouteParam) {
            const mapMethods = {
                get: 'get_',
                put: 'put_',
                patch: 'patch_',
                delete: 'delete_',
            };

            const classMethod = mapMethods[method];

            if (classMethod == null) {
                throw new Error('Invalid method: ' + method);
            }

            const methodBody = `
    /**
     * ${description}
     * @param {object} ctx - Koa context
     * @returns {Promise}
     */        
    async ${classMethod}(ctx, ${subRouteParam}) {
        // sanitization
        ${codeSanitize.join('\n')}

        // business logic
        ${codeImplement.join('\n')}

        // return response
        this.send(ctx, result, payload);
    }
`;
            locals.methods.push(methodBody);
        } else {
            const mapMethods = {
                get: 'query_',
                post: 'post_',
                put: 'putMany_',
                patch: 'patchMany_',
                delete: 'deleteMany_',
            };

            const classMethod = mapMethods[method];

            if (classMethod == null) {
                throw new Error('Invalid method: ' + method);
            }

            const methodBody = `
    /**
     * ${description}
     * @param {object} ctx - Koa context
     * @returns {Promise}
     */        
    async ${classMethod}(ctx) {
        // sanitization
        ${codeSanitize.join('\n')}

        // business logic
        ${codeImplement.join('\n')}

        // return response
        this.send(ctx, result, payload);
    }
`;
            locals.methods.push(methodBody);
        }
    }

    /*
    _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);
        });
    };
    */

    _processUsedModifiers(entity) {
        return entity.modifiers.map((modifier) =>
            this.linker.loadElement(entity.xemlModule, modifier.$xt, modifier.name, true)
        );
    }

    /**
     * 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' };
        compileContext.variables['app'] = { source: 'context', finalized: true, shortFor: 'this.db.app' };

        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.join(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}`);
    }

    _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;