Generate API Spec จาก Swagger JSON

เนื่องจากมีงานที่ต้องทำแต่ API เพียวๆ แล้วเจ้าตัว Swagger ไม่ตอบโจทย์ที่ลูกค้าต้องการ เลยเป็นประเด็นให้เกิด Blog นี้
สำหรับใครที่ไม่รู้จักเจ้า Swagger ก็รองศึกษาดูจาก Swagger

Swagger เรียกๆ ง่ายๆ ก็คือตัวช่วยตัวนึงที่ช่วยให้ API ที่เราทำมีหน้าสำหรับทดสอบและอธิบายแทน Document โดยที่เราไม่ต้องเขียนเอง ช่วยงาน Developer ด้วยกันได้ดีมากๆ (ฟังดูก็ครบถ้วนนิ หึหึ)

เหตุผลหลักๆ ที่ไม่ตอบโจทย์ตามต้องการมีดังนี้

  1. เอกสารอยู่ในรูปแบบของ html เวลาเปิดดูแล้วเหมือนจะสะดวก แต่ถ้าเราต้องดู Schema ควบคู่ไปด้วยจะได้ Scroll mouse ไปๆ มาๆ
  2. เนื่องเอกสารที่ Swagger ทำให้สุดท้ายจะเป็นรูปแบบ html ทำให้เวลาแก้ไขเพิ่มเติมไม่สะดวก
  3. การพิมพ์อธิบาย Condition ต่างๆ ยาวๆ ทำได้ไม่สะดวก
  4. หลายๆ องค์กรยังคงต้องการเอกสารส่งงานที่ดูเป็นทางการอย่าง Word,Excel อยู่

ด้วยเหตุผลด้านบน(เจอมากับตัว) ทำให้ต้องมาทำเอกสารเหมือนเดิม แต่เราไม่อยากเสียเวลาต้องทำใหม่ทั้งหมดเพราะเรามีเจ้า Swagger ทำมาให้เยอะแล้วนิ เลยหาวิธี Convert จากเจ้า Swagger มาเป็น เอกสารอย่าง Word หรือ Excel ให้ได้ดีกว่า

หากท่านใดเจอเครื่องมือที่ดีอยู่แล้วหรือมีวิธีที่ดีกว่าวิธีที่ผมจะอธิบายด้านล่างนี้รบกวนแจ้งให้ทราบด้วยครับ เนื่องจากผมพยายามหาอยู่นานแต่ไม่พบเลยตัดสินใจ เขียนมันเองซะเลย

เจ้า Swagger เนี่ย นอกจากสร้างหน้า Web ให้เราใช้งานง่ายแล้ว แต่มันยังมีเป็นรูปแบบ JSON ให้เรานำไปใช้งานได้ด้วย มี Document อธิยายโครงสร้างชัดเจน ตัวนี้ละที่ช่วยเราได้

อธิบาย JSON Structure แบบสรุปคราวๆ ก่อน

Root ของ JSON จะเป็นดังนี้ สิ่งที่เราสนใจจริงๆ คือ paths และ definitions โดยที่
paths : เป็นรายการของ API และมี Parameter Request/Response บางส่วนที่ไม่ซับซ้อน
definitions : เป็น Schema หรือ Model ของ Parameter Request/Response แบบซับซ้อน

เข้าไปดูที่ paths สิ่งที่เราสนใจจะมีอยู่หลาย Parameter เหมือนกัน โดยส่วนที่ซับซ้อนจะเป็นส่วนของ schema เพราะจะเป็นส่วนที่อ้างถึง Schema ที่อยู่ใน definitions ทำให้เราต้องเขียนข้อมูลไปหา Schema ที่ต้องการในนั้นต่อ

เข้าไปดูที่ definitions จะเป็นรายการของ Schema ทั้งหมด โดยส่วนที่ซับซ้อนจะเป็นส่วนที่เรียกหา definitions เองอีกครั้ง ทำให้ในการเขียน Logic เราควรเขียนให้เป็น Recursive

ดู Document ที่อธิบาย JSON structure เพิ่มได้ที่ Swagger JSON Specification

เริ่มทำเครื่องมือ

ผมจะทำเครื่องมือที่ Convert Swagger JSON ให้ออกมาเป็น Excel API Spec แบบ Simple แต่มี Column ครบพร้อมให้นำไปเขียนหรือแก้ไขเพิ่มได้เลย โดยหน้าตา Excel ที่ได้จะเป็นดังนี้
มี Sheet index ที่รวมรายชื่อของ API ทั้งหมด และใส่สูตร Formula ให้ Click ไปที่ Sheet นั้นได้เลย

มี Column ครบตามที่ API Spec ควรจะมี บอกลำดับชั้นของ Parameter และมี Link Click กลับมา Sheet Index

หากเราเข้าใจ JSON Structure แล้วจะใช้เครื่องมืออะไรทำก็ได้ละ C#, VB, JAVA, Javascript ทางผู้เขียนขอเลือก Javascript ละกัน พอดีกำลังศึกษา Node.js หลักการที่ผมจะทำก็คือจะสร้างตัวแปรเก็บค่าทั้งชื่อ API, Method Type, Description, Request Parameter,Response Parameter ทั้งหมดเก็บไว้ก่อน แล้วค่อยนำไปเขียนสร้างเป็น Excel ออกมาทีเดียว ที่ทำแบบนี้เพื่อให้ง่ายต่อการนำไปใช้ตอนทำ Excel และ Code น่าจะดูสะอาดขึ้น

1. เริ่มแรกก็ npm install package ตัวช่วยก่อน ที่เลือกใช้มีหลักๆ มี 3 ตัว คือ

  1. excel4node : เพราะเราจะสร้างเป็น excel เลยต้องมีตัวช่วยสร้าง excel ให้เรา
  2. request : สำหรับ request json จาก URL Swagger เลย
  3. request-promise : ตัวช่วยทำ async ของ request
npm install excel4node request request-promise

2. เขียน request JSON จาก URL Swagger วิธีดูว่า URL JSON ของ Swagger ของเราคืออะไรดูได้จากหน้า Swagger เองเลยครับ ตามรูป

var rp = require('request-promise');
var swaggerJsonUrl = 'http://xxxxxx:xxxx/xxx/v2/api-docs?group=CustomerMaster';
var json;
var options = {
    uri: swaggerJsonUrl,    
    json: true // Automatically stringifies the body to JSON
};
 console.log('Swagger URL : ' + swaggerJsonUrl);
rp(options)
.then(function (parsedBody) {
    console.log('Get Url Response');
    json = parsedBody;
    console.log('Call generateExcel');
    generateExcel();
})
.catch(function (err) {
    console.log('Error : ' + err);
});

3. สร้าง Class Model สำหรับเก็บ API, Request และ Response

"use strict";
exports.__esModule = true;
var modelService = /** @class */ (function () {
    function modelService() {
        this.module = '';
        this.service = '';
        this.methodType = '';
        this.description = '';
        this.request = [];
        this.response = [];
    }
    return modelService;
}());
exports.modelService = modelService;
var modelRequest = /** @class */ (function () {
    function modelRequest() {
        this.name = '';
        this.paramType = '';
        this.type = '';
        this.lv = '';
        this.description = '';
        this.required = '';
        this.simpleValue = '';
        this.posibleValue = '';
    }
    return modelRequest;
}());
exports.modelRequest = modelRequest;
var modelResponse = /** @class */ (function () {
    function modelResponse() {
        this.name = '';
        this.paramType = '';
        this.type = '';
        this.lv = '';
        this.description = '';
        this.required = '';
        this.simpleValue = '';
        this.posibleValue = '';
    }
    return modelResponse;
}());
exports.modelResponse = modelResponse;

4. เตรียม Function ต่างๆ สำหรับเรียกใช้ในส่วน Function หลัก โดยแยกไว้เป็น Function ย่อยๆ ดังนี้
4.1 Function สำหรับ Recursive หา Parameter ตอน Request

function getDefinitionItemRequest(json ,lvCount, name,paramType) {    
    for (var item in json.definitions) {
        if (item === name) {
            var prop = json.definitions[item];
            for (var pName in prop.properties) {                
                var data = prop.properties[pName];
                if (data.$ref) {
                    
                    var rItem = prop.properties[pName];                    
                    pushDataToModelRequest(prop,pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example,rItem.enum);                    
                                        
                    var defineName = getDefineName(data.$ref);
                    getDefinitionItemRequest(json,(lvCount + 1),defineName,paramType);

                } else {
                    if (data.type == 'object' || data.type == 'array') {
                        // check has child                     
                        for (var cName in data) {
                            var child = data[cName];                                                
                            if (IsString(child)) {
                                if (child.indexOf('#/definitions') !== -1) {
                                    
                                    var defineName = getDefineName(data.$ref);
                                    getDefinitionItemRequest(json,(lvCount + 1),defineName,paramType);                                    
                                } 
                            } else {
                                for (var c2Name in child) {
                                    var child2 = child[c2Name];
                                    if (!IsString(child2)){
                                                                                
                                        var defineName = getDefineName(child2.$ref);                                        
                                        var rItem = child[c2Name];                                        
                                        pushDataToModelRequest(prop,defineName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example,rItem.enum);
                                        getDefinitionItemRequest(json,(lvCount + 1),defineName,paramType);

                                    } else if (IsString(child2) && child2.indexOf('#' > -1)) {
                                                                                
                                        var defineName = getDefineName(child2);                                        
                                        var rItem = child[c2Name];                                        
                                        pushDataToModelRequest(prop,pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example,rItem.enum);
                                        getDefinitionItemRequest(json,(lvCount + 1),defineName,paramType); 
                                    }
                                }
                            }
                        }
                    } else {
                        var rItem = prop.properties[pName];                        
                        pushDataToModelRequest(prop,pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example,rItem.enum);
                    }
                }
            }

            return listDefineRequest;
        }
    }
}

4.2 Function สำหรับ Recursive หา Parameter ตอน Response

function getDefinitionItemResponse(json,lvCount,name,paramType) {    
    for (var item in json.definitions) {
        if (item === name) {
            var prop = json.definitions[item];            
            for (var pName in prop.properties) {
                
                var data = prop.properties[pName];
                
                if (data.$ref) {                    
                    var rItem = prop.properties[pName];                
                    pushDataToModelResponse(pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example);
                    var defineName = getDefineName(data.$ref);
                    getDefinitionItemResponse(json,(lvCount + 1),defineName,paramType);
                    
                } else {
                    if (data.type == 'object' || data.type == 'array') {
                        // check has child                     
                        for (var cName in data) {
                            var child = data[cName];                                                
                            if (IsString(child)) {
                                if (child.indexOf('#/definitions') !== -1) {
                                    var defineName =  getDefineName(data.$ref);
                                    getDefinitionItemResponse(json,(lvCount + 1),defineName,paramType);                                    
                                } 
                            } else {
                                for (var c2Name in child) {
                                    var child2 = child[c2Name];
                                    if (!IsString(child2)){
                                                                                
                                        var defineName = getDefineName(child2.$ref);                                        
                                        var rItem = child[c2Name];                  
                                        pushDataToModelResponse(defineName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example);
                                        getDefinitionItemResponse(json,(lvCount + 1),defineName,paramType);    
                                                                            
                                    } else if (IsString(child2) && child2.indexOf('#' > -1)) {
                                                                                
                                        var defineName = getDefineName(child2);                                        
                                        var rItem = child[c2Name];                                        
                                        pushDataToModelResponse(pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example);
                                        getDefinitionItemResponse(json,(lvCount + 1),defineName,paramType); 
                                    }
                                }
                            }
                        }
                    } else {
                        var rItem = prop.properties[pName];
                        pushDataToModelResponse(pName,paramType,rItem.type,lvCount,rItem.description,rItem.required,rItem.example);
                    }
                }                
            }
            
            return listDefineResponse;
        }
    }
}

4.3 Function สำหรับตรวจสอบว่า Parameter ตัวนี้เป็น Require Filed หรือเปล่าตอน Request

function checkDefinitionRequestIsRequired(requestItem,requestName) {
    if (!requestItem.required) {
        return false;
    }
    for (var reqName in requestItem) {        
        var reqItem = requestItem.required[reqName];
        if (reqItem === requestName) {
            return true;
        } 
    }
    return false;
}

4.4 Function สำหรับตรวจสอบ Data type ว่าเป็น String หรือไม

function IsString(obj) {
    return obj !== undefined && obj != null && obj.toLowerCase !== undefined;
}

4.5 Function สำหรับหาชื่อ Define เพื่อ Recursive

function getDefineName(valueRefStr) {
    var ref = valueRefStr;
    var tmp = ref.split('/');
    var defineName = tmp[tmp.length - 1];
    return defineName;
}

4.6 Function สำหรับ Set ค่า Request สำหรับนำไปสร้าง Excel

function pushDataToModelRequest(jsonPopDefineList,paramName,paramType,dataType,paramLv,paramDesc,paramRequired,paramSimpleValue,paramPosible) {
    var requestModel = new modelService.modelRequest();
    requestModel.name = paramName;
    requestModel.paramType = paramType;
    requestModel.type = dataType;
    requestModel.lv = paramLv;
    if (paramDesc) {
        requestModel.description = paramDesc;
    } else {
        requestModel.description = '';
    }
    if (paramRequired) {
        requestModel.required = paramRequired;
    } else {
        requestModel.required = checkDefinitionRequestIsRequired(jsonPopDefineList,paramName);
    }                                    
    requestModel.simpleValue = paramSimpleValue;
    var posibleStr = '';
    if (paramPosible) {
        for (var eName in paramPosible) {
            posibleStr += paramPosible[eName] + ',';
        }
        if (posibleStr != '') {
            posibleStr = posibleStr.substr(0,posibleStr.length -1);
        }
    }
    requestModel.posibleValue = posibleStr;
    listDefineRequest.push(requestModel);
}

4.7 Function สำหรับ Set ค่า Response สำหรับนำไปสร้าง Excel

function pushDataToModelResponse(paramName,paramType,dataType,paramLv,paramDesc,paramRequired,paramSimpleValue) {
    var responseModel = new modelService.modelResponse();
    responseModel.name = paramName;
    responseModel.paramType = paramType;
    responseModel.type = dataType;
    responseModel.lv = paramLv;
    if (paramDesc) {
        responseModel.description = paramDesc;
    } else {
        responseModel.description = '';
    }
    if (paramRequired) {
        responseModel.required = paramRequired;
    }                                      
    responseModel.simpleValue = paramSimpleValue;
    listDefineResponse.push(responseModel);
}

5. สุดท้าย Function หลักที่ถูกเรียกหลังจาก Request Swagger แล้ว เพื่อเตรียม data และ generate excel โดยขอแบ่งเป็น 2 ส่วน
5.1 ส่วนทำหน้าที่ Prepare Data

for (var path in json.paths) {
        
    var pathItem = json.paths[path];

    for (var method in pathItem) {

        var itemMethod = pathItem[method];         
        // validate
        if (!itemMethod) continue;
        if (!itemMethod.summary) continue;
        if (!itemMethod.operationId) continue;

        var moduleName = '';
        if (itemMethod.tags) {
            moduleName = itemMethod.tags[0]; 
        }
        
        var serviceName = '';
        if (itemMethod.operationId) {
            serviceName = itemMethod.operationId.replace('/','');
        } else {
            if (itemMethod.summary) {
                serviceName = itemMethod.summary.replace('/','');
            }
        }            

        var serviceDesc = '';
        if (itemMethod.description) {
            serviceDesc = itemMethod.description;
        }
            
        // validate for some module or service you want to not generate
        if (skipModuleForGenerate.indexOf(moduleName) > -1) continue;
        if (skipServiceForGenerate.indexOf(serviceName) > -1) continue;

        var serviceModel = new modelService.modelService();
        console.log('module : ' + moduleName);
        serviceModel.module = moduleName;
        console.log('service : ' + serviceName);
        serviceModel.service = serviceName;
        serviceModel.description = serviceDesc;
        serviceModel.methodType = method;
        var param;
        var name;
        var paramType;
        var type;
        var desc;
        var required;
        /* Prepare Request */
        listDefineRequest = [];
        for (var paramName in itemMethod.parameters) {
            param = itemMethod.parameters[paramName];            
            name = param.name;            
            paramType = param.in;            
            type = param.type;            
            desc = param.description;            
            required = false;
            if (param.required) {
                required = true;
            }
            if (param.schema) {
                      
                if (param.schema.$ref) {                    
                    var define = getDefineName(param.schema.$ref);
                    var definition = getDefinitionItemRequest(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.request.push(definition[i]);
                    }
                    listDefineRequest = [];

                } else if (param.schema.items.$ref) {                        
                    var define = getDefineName(param.schema.items.$ref);
                    var definition = getDefinitionItemRequest(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.request.push(definition[i]);
                    }
                    listDefineRequest = [];

                } else {
                    var define = param.schema.description;
                    var definition = getDefinitionItemRequest(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.request.push(definition[i]);
                    }
                    listDefineRequest = [];
                }
    
            } else {
                // LV 1            
                var requestModel = new modelService.modelRequest();
                requestModel.name = name;
                requestModel.paramType = paramType;
                requestModel.type = type;
                requestModel.lv = '1';
                requestModel.description = desc;
                requestModel.required = required;
                requestModel.simpleValue = '';
                serviceModel.request.push(requestModel);
            }        
        }
            
        /* Prepare Response */
        listDefineResponse = [];
        for (var paramName in itemMethod.responses) {
            // filter for response success
            if (paramName != 200) continue;

            param = itemMethod.responses[paramName];        
            name = param.name;            
            paramType = 'body';            
            type = param.type;            
            desc = param.description;            
            required = false;
            if (param.required) {
                required = true;
            }
            if (param.schema) {
                // LV x => for model response multi level
                if (param.schema.$ref) {                    
                    var define = getDefineName(param.schema.$ref);
                    var definition = getDefinitionItemResponse(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.response.push(definition[i]);
                    }
                    listDefineResponse = [];
                } else if (param.schema.items.$ref) {

                    var define = getDefineName(param.schema.items.$ref);
                    var definition = getDefinitionItemResponse(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.response.push(definition[i]);
                    }
                    listDefineResponse = [];
                } else {
                    
                    var define = param.schema.description;
                    var definition = getDefinitionItemResponse(json,1,define,paramType);
                    for (var i in definition) {
                        serviceModel.response.push(definition[i]);
                    }
                    listDefineResponse = [];
                }
    
            } else {
                // LV 1 => for model response single level
                var responseModel = new modelService.modelService();
                responseModel.name = name;
                responseModel.paramType = paramType;
                responseModel.type = type;
                responseModel.lv = '1';
                responseModel.description = desc;
                responseModel.required = required;
                responseModel.simpleValue = '';
                serviceModel.response.push(responseModel);
            }        
        }
    
        serviceList.push(serviceModel);
    }
}

5.2 ส่วนที่ทำหน้าสร้าง Excel ในส่วนนี้ขอแบ่งเป็น 3 ส่วน
5.2.1 ส่วนกำหนด Style ของ Cell

// 1. style for table header
var tHeadStyle = wb.createStyle({
    font: {
        color: '#FFFFFF',        
    },
    border: {
        left: {
            style: 'thin', //§18.18.3 ST_BorderStyle (Border Line Styles) ['none', 'thin', 'medium', 'dashed', 'dotted', 'thick', 'double', 'hair', 'mediumDashed', 'dashDot', 'mediumDashDot', 'dashDotDot', 'mediumDashDotDot', 'slantDashDot']
            color: '#000000' // HTML style hex value
        },
        right: {
            style: 'thin',
            color: '#000000'
        },
        top: {
            style: 'thin',
            color: '#000000'
        },
        bottom: {
            style: 'thin',
            color: '#000000'
        }
    },
    fill:{
        type: 'pattern', // Currently only 'pattern' is implemented. Non-implemented option is 'gradient'
        patternType: 'solid', //§18.18.55 ST_PatternType (Pattern Type)
        bgColor: '#000000', // HTML style hex value. defaults to black
        fgColor: '#000000' // HTML style hex value. defaults to black.
    }
});
// 2. style for table body cell with indent
var cellIndentStyle = wb.createStyle({
    alignment:{
        indent:1
    }    
});
// 3. style for table body cell
var cellBorderStyle = wb.createStyle({   
    border: { // §18.8.4 border (Border)
        left: {
            style: 'thin', //§18.18.3 ST_BorderStyle (Border Line Styles) ['none', 'thin', 'medium', 'dashed', 'dotted', 'thick', 'double', 'hair', 'mediumDashed', 'dashDot', 'mediumDashDot', 'dashDotDot', 'mediumDashDotDot', 'slantDashDot']
            color: '#000000' // HTML style hex value
        },
        right: {
            style: 'thin',
            color: '#000000'
        },
        top: {
            style: 'thin',
            color: '#000000'
        },
        bottom: {
            style: 'thin',
            color: '#000000'
        },
        diagonal: {
            style: 'thin',
            color: '#000000'
        },
        diagonalDown: true,
        diagonalUp: true,
        outline: true
    }
});

5.2.2 ส่วนสำหรับสร้าง Sheet Index

// start loop set table body of sheet index
for (let i = 0;i < index.length; i++) {     var obj = index[i];     ws.cell(row, 1).number(row -1);     ws.cell(row, 1).style(cellBorderStyle);     ws.cell(row, 2).string(obj.module);     ws.cell(row, 2).style(cellBorderStyle);     ws.cell(row, 3).string(obj.service);     ws.cell(row, 3).style(cellBorderStyle);     var serviceName = obj.service;     // for protect limit sheet name of excel     if (serviceName.length > 30) {
        serviceName = serviceName.substring(0,30);
    }

    // for link goto sheet service
    ws.cell(row, 4).formula('=HYPERLINK("#' + serviceName + '!A1","' + obj.service + '")');
    ws.cell(row, 4).style(cellBorderStyle);    
    row++;
}

5.2.3 ส่วนสำหรับสร้าง Sheet API Specification และ Save ออกมาเป็น Excel File

/* create excel specification follow api */
for (let service in serviceList) {
        
    console.log('module : ' + serviceList[service].module + ' / service : ' + serviceList[service].service);        
    var row = 1;
    var serviceName = serviceList[service].service;

    // for protect limit sheet name of excel
    if (serviceName.length > 30) {
        serviceName = serviceName.substring(0,30);
    }

    // create new sheet
    var ws = wb.addWorksheet(serviceName);
    
    ws.cell(row, 1,row,3,true).string('Module : ' + serviceList[service].module);

    // for back to index sheet
    ws.cell(row, 11).formula('=HYPERLINK("#index!A1","Back to index")');

    row++;
    ws.cell(row, 1,row,3,true).string('Service Name : ' + serviceList[service].service);
    row++;
    ws.cell(row, 1,row,3,true).string('Method Type : ' + serviceList[service].methodType);
    row++;
    ws.cell(row, 1,row,3,true).string('Description : ' + serviceList[service].description);
    row++;

    var count = 1;
    // set table header of request parameter
    ws.cell(row, 1).string('Request');
    row++;
    ws.cell(row, 1).string('Parameter');
    ws.cell(row,1).style(tHeadStyle);
    ws.cell(row, 2).string('Parameter Type');
    ws.cell(row,2).style(tHeadStyle);
    ws.cell(row, 3).string('Level');
    ws.cell(row,3).style(tHeadStyle);
    ws.cell(row, 4).string('Data Type');
    ws.cell(row,4).style(tHeadStyle);
    ws.cell(row, 5).string('Length');
    ws.cell(row,5).style(tHeadStyle);
    ws.cell(row, 6).string('O/M');
    ws.cell(row,6).style(tHeadStyle);
    ws.cell(row, 7).string('Description');
    ws.cell(row,7).style(tHeadStyle);
    ws.cell(row, 8).string('Simple Value');
    ws.cell(row,8).style(tHeadStyle);
    ws.cell(row, 9).string('Possible Value');
    ws.cell(row,9).style(tHeadStyle);
    ws.cell(row, 10).string('Formula/Remark');
    ws.cell(row,10).style(tHeadStyle); 
    row++;

    // start table body of request parameter
    for (let req in serviceList[service].request) {        
            
        var item = serviceList[service].request[req];
        
        // Column Parameter
        if (item.name) {
            ws.cell(row, 1).string(item.name);              
        } else {
            ws.cell(row, 1).string('');      
        }
        // check level of parameter for use style
        if (item.lv) {                            
            if (Number(item.lv) > 1){
                cellIndentStyle.alignment.indent = Number(item.lv) - 1;      
                ws.cell(row, 1).style(cellIndentStyle);                  
            }    
        }        
        ws.cell(row, 1).style(cellBorderStyle);     
        
        // Column Parameter Type (header , body)
        if (item.paramType) {
            ws.cell(row, 2).string(item.paramType);      
        } else {
            ws.cell(row, 2).string('');
        }     
        ws.cell(row, 2).style(cellBorderStyle);          
        
        // Column Parameter Level
        if (item.lv) {
            ws.cell(row, 3).number(Number(item.lv));               
        } else {
            ws.cell(row, 3).number(0);      
        } 
        ws.cell(row, 3).style(cellBorderStyle);           
        
        // Column Type (string , integer , ....)
        if (item.type) {
            ws.cell(row, 4).string(item.type);      
        } else {
            ws.cell(row, 4).string('');      
        }             
        ws.cell(row, 4).style(cellBorderStyle);     
        
        // Column Length for other condition or mapping db column size
        ws.cell(row, 5).style(cellBorderStyle);
        
        // Column O/M (optional , mandatory)
        if (item.required) {
            ws.cell(row, 6).string('M');
        } else {
            ws.cell(row, 6).string('O');
        }
        ws.cell(row, 6).style(cellBorderStyle);
        
        // Column Description
        if (item.description) {
            ws.cell(row, 7).string(item.description);      
        } else {
            ws.cell(row, 7).string('');      
        } 
        ws.cell(row, 7).style(cellBorderStyle); 
        
        // Column Simple value
        if (item.simpleValue) {
            ws.cell(row, 8).string(item.simpleValue.toString());
        } else {
            ws.cell(row, 8).string('');
        }   
        ws.cell(row, 8).style(cellBorderStyle);
        
        // Column Possible Value not include in swagger
        if (item.posibleValue) {
            ws.cell(row, 9).string(item.posibleValue);      
        } else {
            ws.cell(row, 9).string('');      
        } 
        ws.cell(row, 9).style(cellBorderStyle);    

        // Column Formula/Remark not include in swagger
        ws.cell(row, 10).style(cellBorderStyle);
        count ++;
        row++;
    }
    row++;

    // set table header of response parameter
    ws.cell(row, 1).string('Response');
    row++;
    ws.cell(row, 1).string('Parameter');
    ws.cell(row,1).style(tHeadStyle);
    ws.cell(row, 2).string('Parameter Type');
    ws.cell(row,2).style(tHeadStyle);
    ws.cell(row, 3).string('Level');
    ws.cell(row,3).style(tHeadStyle);
    ws.cell(row, 4).string('Data Type');
    ws.cell(row,4).style(tHeadStyle);
    ws.cell(row, 5).string('Length');
    ws.cell(row,5).style(tHeadStyle);
    ws.cell(row, 6).string('O/M');
    ws.cell(row,6).style(tHeadStyle);
    ws.cell(row, 7).string('Description');
    ws.cell(row,7).style(tHeadStyle);
    ws.cell(row, 8).string('Simple Value');
    ws.cell(row,8).style(tHeadStyle);
    ws.cell(row, 9).string('Possible Value');
    ws.cell(row,9).style(tHeadStyle);
    ws.cell(row, 10).string('Formula/Remark');
    ws.cell(row,10).style(tHeadStyle);
    row++;

    // start table body of response parameter
    for (let res in serviceList[service].response) {
                    
        var item = serviceList[service].response[res]; 
        
        // Column Parameter Name
        if (item.name) {
            ws.cell(row, 1).string(item.name);                       
        } else {
            ws.cell(row, 1).string('');      
        } 
        // check level of parameter for use style
        if (item.lv) {                              
            if (Number(item.lv) > 1){
                cellIndentStyle.alignment.indent = Number(item.lv) - 1;
                ws.cell(row, 1).style(cellIndentStyle);
            }                                              
        }      
        ws.cell(row, 1).style(cellBorderStyle); 
        
        // Column Parameter Type (header,body)
        if (item.paramType) {
            ws.cell(row, 2).string(item.paramType);      
        } else {
            ws.cell(row, 2).string('');
        }
        ws.cell(row, 2).style(cellBorderStyle);
        
        // Column Parameter Level
        if (item.lv) {
            ws.cell(row, 3).number(Number(item.lv));      
        } else {
            ws.cell(row, 3).number(0);      
        }    
        ws.cell(row, 3).style(cellBorderStyle);    
        
        // Column Type (string , integer , ....)
        if (item.type) {
            ws.cell(row, 4).string(item.type);      
        } else {
            ws.cell(row, 4).string('');      
        }     
        ws.cell(row, 4).style(cellBorderStyle);

        // Column Length for other condition or mapping db column size
        ws.cell(row, 5).style(cellBorderStyle);                  

        // Column O/M (optional , mandatory)
        if (item.required) {
            ws.cell(row, 6).string('M');
        } else {
            ws.cell(row, 6).string('O');
        }
        ws.cell(row, 6).style(cellBorderStyle);
        
        // Column Description
        if (item.description) {
            ws.cell(row, 7).string(item.description);      
        } else {
            ws.cell(row, 7).string('');      
        } 
        ws.cell(row, 7).style(cellBorderStyle);
        
        // Column Simple Value
        if (item.simpleValue) {
            ws.cell(row, 8).string(item.simpleValue.toString());
        } else {
            ws.cell(row, 8).string('');
        }        
        ws.cell(row, 8).style(cellBorderStyle);
        
        // Column Possible Value not include in swagger                          
        ws.cell(row, 9).style(cellBorderStyle);
        
        // Column Formula/Remark not include in swagger
        ws.cell(row, 10).style(cellBorderStyle);
        count ++;
        row++;
    }        
}

// save excel file
wb.write(excelFileName, function (err, stats) {
    if (err) {
        console.error(err);
    }  else {
        console.log(stats); // Prints out an instance of a node.js fs.Stats object
    }
});

คาดว่าบทความนี้จะช่วยบางท่านได้ไม่มากก็น้อย ขอบคุณครับ

Download ตัวอย่าง Output Excel ที่ได้จาก Output Excel
Download Source Code ทั้งหมดได้ที่ github

VSCODE Connect MSSQL

หลังๆ มานี้โปรแกรม SQL Management Studio เริ่มมีขนาดใหญ่ขึ้นเรื่อยๆ ละ หลังสุดที่ผมเห็นขนาดตัวลงอยู่ที่ 1GB กว่าๆ ซึ้งผมว่ามันใหญ่มากกกกก

ผมเลยรองหา Tools อะไรสักตัวที่ไม่ใหญ่มาก และสามารถทำงานกับ MSSQL พื้นฐานได้ครบ (SELECT , INSERT , UPDATE , DELETE , Call Function/Stored Procedure) แล้วผมก็เจอ Extension บน VSCODE ซึ้งทำงานได้ OK เลยทีเดียว

  1. แน่นอนต้องมี VSCODE ก่อนเลย วิธีติดต่อก็ตามนี้เลยครับ
    ไปที่เมนู Extension เลยครับ แล้วพิมพ์ชื่อ “mssql” install-extensionหรือสามารถ Instrall ผ่าน vscode market place ได้ที่นี
  2. Change Language ของ File เราให้เป็น SQL ซะก่อน
    1. พิมพ์ Ctrl + P พิมพ์ Change Language Mode แล้ว Enter change-language
    2. พิมพ์ sql แล้ว Enter
      change-language-sql
  3. กำหนด connection ครับ
    Ctrl+P แล้วพิมพ์ mssql แล้ว Enter

    1. ขั้นแรกเรายังไม่เคยสร้าง connection ไว้ จะแสดงให้เรา create profile ครับ
      Enter ที่ Create Connection Profile แล้ว Enter
    2. กำหนด Hostname หรือ Server Database ที่จะ Connect แล้ว Enter
    3. กำหนดชื่อ Database แล้ว Enter
    4. กำหนดรูปแบบการ Authentication แล้ว Enter
    5. กำหนด Username สำหรับ Connect แล้ว Enter
    6. กำหนด Password สำหรับ Connect แล้ว Enter
    7. กำหนดว่าให้จำ Password เราไว้หรือไม แล้ว Enter
    8. กำหนดชื่อ Profile นี้ แล้ว Enter
    9. เมื่อกำหนดครบแล้วจะแสดงข้อความแบบนี้แสดงว่า Create และ Connect เรียบร้อย

      สังเกตุ taskbar ด้านล่างจะแสดงการ connect ของเราตามที่เรากำหนด
  4. Create Profile และ Connect เรียบร้อยพร้อมใช้งาน

วิธีการใช้งานก็ไม่ยุ่งยากครับ

  1. พิมพ์คำสั่ง SQL เลยครับ

    สังเกตุเห็นอะไรไมครับ มี intellisense แสดง Table กับ Column ด้วยครับ !!! เจ๋งมาก
  2. พิมพ์เสร็จแล้วเวลา Execute กด Ctrl+P พิมพ์ mssql เลือก Execute Query หรือ Ctrl + Shift + E
  3. แสดงผลลัพธ์เป็น Grid และ Message

Tip เจ๋งๆ ครับ ที่ผมชอบมาก

Extension ตัวนี้สามารถ Export Result ที่ได้ออกมาได้เป็น Format Excel, CSV และ JSON ครับ ด้วยวิธีง่ายๆ เลยครับ แค่คลิกขวา เลือก Format ที่ต้องการ

แค่นี้เองครับง่ายๆ เจ๋งมาก!!!

โดยสามารถเลือกเฉพาะ Column

หรือหลายๆ Column ตามที่ต้องการได้เลย

ขอบคุณครับ
Ref : https://docs.microsoft.com/en-us/sql/linux/sql-server-linux-develop-use-vscode

Remote Access MySQL บน DigitalOcean

พอดีเพิ่งเริมได้เล่น Cloud แบบจริงๆ จังๆ ก็เลยเลือกใช้ของ DigitalOcean เนื่องจากถูกมาก ^^ และหาข้อมูลใช้งานง่าย ที่ต้องทำ Remote Access นั้นเนื่องจากหลังจากสร้าง Droplets (เลือก Ubuntu) แล้วเวลาใช้งานต้องทำงานผ่าน Command เท่านั้นซึ่งไม่สะดวกเลย ก็เลยหาวิธีใช้งานให้ง่ายเวลาจะเข้าไปจัดการกับ Database
โอเคเริ่มเลย ก็เปิด putty แล้วก็ shell command กันเลย

ติดตั้ง MySQL
1. เข้ามาแล้วก็ update กันก่อน


$ sudo apt-get update 

2. ติดตั้ง MySQL


sudo apt-get install mysql-server

3. Configuration MySQL และ Set Password


sudo mysql_secure_installation

4. Active MySQL


sudo mysql_install_db

5. ทดสอบ MySQL ด้วย


service mysql status

Enable Remote Access MySQL
การ Enable Remote Access MySQL ทำเพื่อให้เราสามารถ Connect จากโปรแกรมบนเครื่องเราได้
พิมพ์คำสั่ง เพื่อเข้าไปแก้ไข config


sudo vi /etc/mysql/my.cnf

comment 2 บรรทัดนี้


#bind-address = 127.0.0.1
#skip-external-locking

จากนั้น Restart MySQL ด้วยคำสั่ง


sudo service mysql restart

ทำการ Connect MySQL ด้วยคำสั่ง


mysql –u root -p
ระบุ Password ที่เราได้กำหนดไว้ตอน Configuration

ทำการ Grant permission ให้ User root


GRANT ALL PRIVILEGES ON *.* TO root@'%' IDENTIFIED BY 'YOUR_PASSWORD';
FLUSH PRIVILEGES;

เรียบร้อย ทดสอบเปิดโปรแกรม Connect MySQL บนเครื่อง
กำหนด IP เป็น IP ของ DigitalOcean ที่ได้ กำหนด User เป็น root และ Password ตามที่กำหนดไว้ แล้วกด Open
1

แสดง Popup จากเตือน ว่าเราไม่ได้ใช้ SSL กด OK
2

แสดง Default Database เรียบร้อย พร้อมใช้งาน
3

ขอบคุณครับ

ลองใช้ reCAPTCHA ใหม่ของ Google

CAPTCHA สำหรับ web developer หลายๆ คนคงรู้ความหมายอยู่แล้ว ผมขอสรุปสั่นๆ แล้วกันครับ

CAPTCHA คือเครื่องมือช่วยป้องกัน spam หรือ bot ของ website ครับ ซึ่งไม่นานมานี้ Google ได้พัฒนา reCAPTCHA ให้ใช้งานได้ง่ายขึ้น(จริงข่าวออกมาซักพักละ) โดย reCAPTCHA version ใหม่ตัวนี้ผู้ใช้ไม่จำเป็นต้องพิมพ์ตัวอักษรตามที่เห็นอีกต่อไป เปลี่ยนเป็นการติ๊กถูกว่าเราไม่ใช้ bot ซึ่งการตรวจสอบนั้นทาง Google จะประเมินความเสี่ยงและแยกแยะเองว่าเป็นมนุษย์จริงๆ หรือไม   แต่ถึงอย่างนั้นหากระบบไม่สามารถแยกแยะได้ว่าเป็นมนุษย์หรือ bot ระบบก็จะแสดงตัวอักษรมาให้พิมพ์ตามเดิม ดูแล้วน่าจะสะดวกกับผู้ใช้มากขึ้น

การใช้งาน reCAPTCHA ของ Google นั้นต้อง register ก่อนนะครับเพื่อขอรับ Website Key ในการนำไปใช้ วิธีการนั้นก็ง่ายๆ ครับตามนี้เลย

1. เข้าไปยังเว็บ https://www.google.com/recaptcha/intro/index.html

2. คลิกที่ปุ่ม Get reCAPTCHA ดังรูป

3. Login เข้าใช้งานด้วย Account gmail

4. กรอกข้อมูลลงในหน้าต่างดังรูป แล้วคลิก Register

กรอก Label และ Domain ที่จะนำไปใช้

5. เรียบร้อยได้ Website Key มาตามรูป

Website Key และ Secret Key

การเขียน Code เรียกใช้ reCAPTCHA

ตัวอย่างที่ 1 เรียกใช้ผ่าน HTML

เรียกใช้งานผ่าน Tag HTML

ตัวอย่างที่ 2 เรียกใช้งานผ่าน javascript

เรียกใช้งานด้วย javascript

ผลลัพท์ที่ได้

ผลลัพธ์

หากต้องการศึกษาการใช้งานหรือคำสั่งต่างๆ เพิ่มเติม ดูได้ที่

https://developers.google.com/recaptcha/intro

การทำ Dropdownlist(Select) Type และ Subtype

ทุกคนที่เคยเขียนโปรแกรมต้องเคยทำ Dropdownlist 2 หรือ 3 อันที่มีความสัมพันธ์เกี่ยวข้องกันอยู่บ้าง

เช่นเลือกประเภทหลักแล้ว ให้แสดงเฉพาะประเภทย่อยที่เกี่ยวข้องตามที่เลือกประเภทหลักเท่านั้น

โดยตัวอย่างนี้ผมจะทำ Dropdownlist 3 ระดับให้ดูเป็นตัวอย่างครับ

สิ่งที่ผมนำมาใช้ในครั้งนี้ (เรียกใช้ผ่าน CDN)

1. jQuery  (https://code.jquery.com/jquery-1.11.0.min.js)

2. CSS ของ Bootstrap (http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css)

3. Database สำหรับทดสอบผมเลือกใช้ MySQL


ขั้นตอนที่ 1 เริ่มโดยสร้าง Database เตรียมพร้อมไว้ก่อน

ในตัวอย่างนี้ผมใช้ Table ทั้งหมด 3 Table ด้วยกัน

– Table 1 ชื่อ tbl_type เก็บประเภทหลัก (script สร้างตามรูป)

create table tbl_type

– Table 2 ชื่อ tbl_sub_type เก็บประเภทย่อย (script สร้างตามรูป)

create table tbl_sub_type

– Table 3 ชื่อ tbl_product เก็บข้อมูลสินค้า (script สร้างตามรูป)

create table tbl_product

 

ขั้นตอนที่ 2 เขียนโปรแกรม(php) สำหรับ select ข้อมูลออกมาให้กับ Dropdownlist

php connect mysql generate datasource

 

ขั้นตอนที่ 3  เขียน HTML สำหรับ Dropdownlist สำหรับทดสอบใช้งาน

html with dropdownlist

ขั้นตอนสุดท้าย เขียน jQuery สำหรับให้ Dropdownlist ทำงาน

– เมื่อ page load เรียบร้อยให้เตรียมข้อมูลลงที่ Dropdownlist Type

jQuery onloaded

– function เมื่อเลือก Dropdownlist Type

function onTypeChange (parametet1 , parameter2)

– function เมื่อเลือก Dropdownlist Subtype

function onSubtypeChange (parameter1)

– function clear option เมื่อมีการเปลี่ยนแปลง

function clearOptionItem (parameter1)

– function พระเอกของเรา ติดต่อ PHP ผ่าน Ajax เพื่อสร้าง Item ใน Dropdownlist

function callAjax for generate dropdownlist item


สามารถดูตัวอย่างได้ที่ Demo
Download Code ตัวอย่างได้จาก SourceCode

การสร้าง Dynamic Table

ผมได้มีโอกาสเขียนโปรแกรมออกแบบการทำงานให้ยืดหยุ่นตามที่ User ต้องการ สิ่งที่ User ต้องการคืออยากจะสร้าง Table ขึ้นเองโดยที่สามารถกำหนดข้อความส่วนต่างๆ ได้ตามต้องการ ผมเห็นว่าเป็นเรื่องน่าสนใจสำหรับผู้พัฒนาใหม่ๆ เพราะสามารถนำไปใช้พัฒนาเป็นลูกเล่นได้ทั้งในส่วน Bankend และ Frontend จึงอยากนำตัวอย่างที่ผมทำมาให้ดูกันครับ

สิ่งจำเป็นสำหรับตัวอย่างนี่ที่ผมใช้

1. jQuery (แน่นอนเว็บสมัยนี้ขาดมันไม่ได้เลย มันช่วยให้การเขียน javascript สะดวกสบายขึ้นเยอะ)

2. bootstrap ทั้ง css และ js

3. ตัวช่วยในการสร้าง modal ของ bootstrap คือ modal.js


เริ่มต้นด้วยส่วนของ html (index.html)

dynamic-table

ตามมาด้วย function ต่างๆ ของ Javascript

1.  function เมื่อปุ่ม button clicked

function event button clicked

2.  function สำหรับแสดง popup ให้ระบุจำนวน column และ row

function popup confirm

function popup confirm

3.  function ที่รอรับการ call back จากหน้า popup

js2

function รอรับการ call bank จาก popup

4.  function พระเอกของเรา สร้าง table ขึ้นมา

function create table

function create table


ดูตัวอย่างได้จากที่นี่ครับ Demo
Download code ตัวอย่างได้จาก SourceCode