modeler/GraphQL.js

"use strict";

const path = require('path');
const { _, naming }  = require('@genx/july');
const { fs } = require('@genx/sys');

const { toGraphQLType } = require('./graphql/lang');

/**
 * GraphQL schemas modeler.
 * @class
 */
class GraphQLModeler {
    /**     
     * @param {object} context   
     * @property {GemlLinker} context.linker - Geml linker
     * @property {object} context.modelPath - Generated model output path
     * @property {object} context.manifestPath - Entities manifest output path
     * @param {Connector} connector      
     */
    constructor(context, linker, connector) {       
        this.linker = linker;
        this.outputPath = context.modelPath;
        this.manifestPath = context.manifestPath;

        this.connector = connector;        
    }

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

        //this._generateSchemaModel(schema);        
        this._generateGraphQLModel(schema);
    }

    /*
    _generateSchemaModel(schema) {
        let capitalized = pascalCase(schema.name);

        let locals = {
            driver: this.connector.driver,
            className: capitalized,
            schemaName: schema.name,
            entities: JSON.stringify(Object.keys(schema.entities))
        };

        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) {
        _.forOwn(schema.entities, (entity, entityInstanceName) => {
            _.forOwn(entity.fields, (field, fieldName) => {
                if (field.type === 'enum') {

                }
            });
        });
    }

    _generateGraphQLModel(schema) {
        const generated = new Set();

        const typeDefs = [];

        _.forOwn(schema.entities, (entity, entityInstanceName) => {            
            let capitalized = naming.pascalCase(entityInstanceName);                  

            let fields = _.map(entity.fields, (field, fieldName) => {  
                if (fieldName === entity.key) {
                    return `${fieldName}: ID!`;
                }

                const typeInfo = toGraphQLType(field);    
                
                if (typeInfo.newType) {
                    if (!generated.has(typeInfo.newType)) {
                        generated.add(typeInfo.newType);

                        switch (typeInfo.typeName) {
                            case 'scalar':
                                typeDefs.push(`scalar ${typeInfo.newType}`);
                                break;

                            case 'enum':
                                typeDefs.push(`enum ${typeInfo.newType} {
    ${typeInfo.values.map(v => _.snakeCase(v).toUpperCase()).join('\n    ')}
}`);
                                break;

                            default:
                                throw new Error(`Unsupported graphql type: ${typeInfo.newType}`);
                        }
                    } 
                }
                
                return `${fieldName}: ${typeInfo.type}`;
            });

            if (_.isEmpty(!entity.associations)) {
                _.each(entity.associations, (assoc, anchor) => {
                    const typeName = naming.pascalCase(assoc.entity);

                    if (assoc.list) {
                        fields.push(`${anchor}_: [${typeName}!]`);
                    } else {
                        fields.push(`${anchor}_: ${typeName}`);
                    }
                });
            }

            let classCode = `type ${capitalized} {
    ${fields.join('\n    ')}
}`;                         

            typeDefs.push(classCode);            
        });

        let modelFilePath = path.resolve(this.manifestPath, 'graphql', schema.name + '.graphql');
        fs.ensureFileSync(modelFilePath);
        fs.writeFileSync(modelFilePath, typeDefs.join('\n\n'));

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

    _generateEntityManifest(schema) {
        let entities = Object.keys(schema.entities).sort().reduce((result, v) => { result[v] = {}; return result; }, {});
        /*
        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);
    }

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

module.exports = GraphQLModeler;