Repository: hiowenluke/noapi Branch: master Commit: 1b674c17c886 Files: 72 Total size: 30.1 KB Directory structure: gitextract_cri7q73q/ ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── examples/ │ ├── 01-hello-world/ │ │ ├── biz/ │ │ │ ├── about.js │ │ │ └── say/ │ │ │ └── hi.js │ │ └── index.js │ ├── 02-complex-url-params/ │ │ ├── biz/ │ │ │ └── foo/ │ │ │ └── bar.js │ │ └── index.js │ ├── 03-returning-an-error/ │ │ ├── biz/ │ │ │ └── do/ │ │ │ └── somethingIsWrong.js │ │ └── index.js │ ├── 04-throwing-an-error/ │ │ ├── biz/ │ │ │ └── do/ │ │ │ └── say/ │ │ │ └── hi.js │ │ └── index.js │ ├── 05-api-directory/ │ │ ├── api/ │ │ │ ├── about.js │ │ │ └── say/ │ │ │ └── hi.js │ │ ├── biz/ │ │ │ ├── __lib/ │ │ │ │ └── reverse.js │ │ │ ├── about.js │ │ │ └── say/ │ │ │ ├── __lib/ │ │ │ │ ├── decode.js │ │ │ │ └── encode.js │ │ │ └── hi.js │ │ └── index.js │ ├── 06-with-database/ │ │ ├── biz/ │ │ │ ├── about.js │ │ │ └── user/ │ │ │ ├── get.js │ │ │ ├── kill.js │ │ │ ├── list.js │ │ │ ├── login.js │ │ │ ├── logout.js │ │ │ └── register.js │ │ ├── db/ │ │ │ └── index.js │ │ └── index.js │ ├── 07-static-resources/ │ │ ├── biz/ │ │ │ └── about.js │ │ ├── index.js │ │ └── public/ │ │ ├── images/ │ │ │ └── 1.jsx │ │ ├── index.html │ │ └── nav/ │ │ ├── index.html │ │ └── main.html │ ├── 99-options/ │ │ ├── config.js │ │ ├── index.js │ │ └── src/ │ │ └── about.js │ └── noapi.js ├── index.js ├── package.json ├── src/ │ ├── biz/ │ │ ├── do.js │ │ ├── index.js │ │ └── paramsCache.js │ ├── config.js │ ├── data.js │ ├── index.js │ └── server/ │ ├── cross.js │ ├── index.js │ ├── public/ │ │ ├── index.js │ │ └── mime.js │ └── routes/ │ ├── index.js │ └── parseQueryStr.js └── test/ ├── forExamples/ │ ├── index.js │ └── testCases/ │ ├── 01-hello-world.js │ ├── 02-complex-url-params.js │ ├── 03-returning-an-error.js │ ├── 04-throwing-an-error.js │ ├── 05-api-directory.js │ ├── 06-with-database.js │ ├── 07-static-resources.js │ └── 99-options.js ├── index.js └── tests/ ├── api-folder/ │ ├── api/ │ │ └── about.js │ ├── index.js │ └── test/ │ └── cases.js ├── default/ │ ├── biz/ │ │ ├── get.js │ │ └── post.js │ ├── index.js │ └── test/ │ └── cases.js └── index.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # Matches multiple files with brace expansion notation # Set default charset [*.{js,jsx,html,sass}] charset = utf-8 indent_style = tab indent_size = 4 trim_trailing_whitespace = true ================================================ FILE: .gitignore ================================================ node_modules/ module_test/ coverage/ .DS_Store .idea package-lock.json .temp/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Owen Luke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Noapi A high-performance and easy-to-use web API framework for [Node.js](https://nodejs.org). Noapi loads directory "biz" as a web API server, each file in it defines and handles an API, so that you can focus on writing business function code. Noapi is simple enough that you just take it "out of the box". ## Install ```sh npm install noapi --save ``` ## TRY IT! (in under 3 minutes) ### 0. Initialize a demo project ```sh mkdir ./noapi-demo && cd ./noapi-demo npm init -y npm install noapi --save ``` Create the core directory "**biz**" ```sh mkdir biz ``` ### 1. Create a biz file Create file "./biz/say/hi.js". It defines an api "/say/hi" and handles it. ```js module.exports = async (name, age) => { return {msg: `Hi, I am ${name}, ${age} years old.`}; }; ``` ### 2. Create index.js ```js require('noapi')(); ``` ### 3. Run ```sh node index.js Server default is listening on port 3000 ``` Visit the url [http://localhost:3000/say/hi?age=100&name=owen]() to see the result: ```json { "success": true, "data": { "msg": "Hi, I am owen, 100 years old." } } ``` The order of the parameters in the url can be arbitrary. ## Examples * [01 hello world](./examples/01-hello-world) * [02 complex url params](./examples/02-complex-url-params) * [03 returning an error](./examples/03-returning-an-error) * [04 throwing an error](./examples/04-throwing-an-error) * [05 api directory](./examples/05-api-directory) * [06 with database](./examples/06-with-database) * [07 static resources](./examples/07-static-resources) * [99 options](./examples/99-options) ## Options It can be omitted if it is the default value as below. ```js const name = 'default'; const dir = './biz'; const host = 'localhost'; const port = 3000; const isSilence = false; // The number and order of parameters can be arbitrary noapi(name, dir, host, port, isSilence); // It is equivalents to: // const options = { // name: 'default', // dir: './biz', // host: 'localhost', // port: 3000, // isSilence: true, // }; // // noapi(options); ``` See "[examples/99-options](./examples/99-options)" to learn about it. ## Biz directory Each file in the biz directory defines and handles an API. All files in the biz directory make up the API list. ```js /root /biz /say hi.js // api: /say/hi about.js // api: /about index.js ``` If there are some non-api files (only be used internally) in the biz directory, this will make the API list unclear. Then you should use the **api directory**, just create an empty file (or with the description of this api) to define an api. See "[examples/05-api-directory](./examples/05-api-directory)". ```js /root /api /say hi.js // api: /say/hi about.js // api: /about /biz /__lib /say /__lib hi.js tools.js // non-api check.js // non-api about.js init.js // non-api index.js ``` ## Performance Noapi is a high-performance web API framework, faster than [Koa](https://github.com/koajs/koa), [Express](https://github.com/expressjs/express). See [API Framework Performance PK](https://github.com/hiowenluke/api-frameworks-performance-pk) ## Test ```sh git clone https://github.com/hiowenluke/noapi cd noapi npm install npm test ``` ## License [MIT](LICENSE) Copyright (c) 2019, Owen Luke ================================================ FILE: examples/01-hello-world/biz/about.js ================================================ // Test // http://localhost:3000/about // Result // { // "success": true, // "data": { // "version": "1.0.0" // } // } const fn = async () => { return {version: '1.0.0'}; }; module.exports = fn; ================================================ FILE: examples/01-hello-world/biz/say/hi.js ================================================ // Test // http://localhost:3000/say/hi?age=100&name=owen // Result // { // "success": true, // "data": { // "msg": "Hi, I am owen, 100 years old." // } // } // The order of the function parameters does NOT have to be // the same as the order of the url parameters. const fn = async (name, age) => { return {msg: `Hi, I am ${name}, ${age} years old.`}; }; module.exports = fn; ================================================ FILE: examples/01-hello-world/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/02-complex-url-params/biz/foo/bar.js ================================================ // Test // http://localhost:3000/foo/bar?name=owen&obj={"date":"2019-05-01"}&arr=[1,"abc",{"tel":12345678}] // Result // { // "success": true, // "data": { // "name": "owen", // "obj": { // "date": "2019-05-01" // }, // "arr": [ // 1, // "abc", // { // "tel": 12345678 // } // ] // } // } const fn = async (name, obj, arr) => { return {name, obj, arr}; }; module.exports = fn; ================================================ FILE: examples/02-complex-url-params/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/03-returning-an-error/biz/do/somethingIsWrong.js ================================================ // Test // http://localhost:3000/do/somethingIsWrong // Result // { // "success": false, // "error": "something is wrong" // } const fn = async () => { return {error: 'something is wrong'}; }; module.exports = fn; ================================================ FILE: examples/03-returning-an-error/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/04-throwing-an-error/biz/do/say/hi.js ================================================ // Test // http://localhost:3000/do/say/hi const fn = async () => { throw new Error('something is wrong'); }; module.exports = fn; ================================================ FILE: examples/04-throwing-an-error/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/05-api-directory/api/about.js ================================================ // This file can be empty, or with the description of this api. // See ./say/hi.js ================================================ FILE: examples/05-api-directory/api/say/hi.js ================================================ /** * @test http://localhost:3000/say/hi?age=100&name=owen * @returns { "success": true, "data": { "msg": "Hi, I am owen, 100 years old.", "reversed": ".dlo sraey 001 ,newo ma I ,iH", "encoded": "SGksIEkgYW0gb3dlbiwgMTAwIHllYXJzIG9sZC4=" } } * @param {String} name user's name with a maximum length of 32 * @param {Integer} age user's age with a maximum of 200 * */ ================================================ FILE: examples/05-api-directory/biz/__lib/reverse.js ================================================ const fn = (str) => { return str.split("").reverse().join(""); }; module.exports = fn; ================================================ FILE: examples/05-api-directory/biz/about.js ================================================ const fn = async () => { return {version: '1.0.0'}; }; module.exports = fn; ================================================ FILE: examples/05-api-directory/biz/say/__lib/decode.js ================================================ const fn = (b64Encoded) => { return Buffer.from(b64Encoded, 'base64').toString(); }; module.exports = fn; ================================================ FILE: examples/05-api-directory/biz/say/__lib/encode.js ================================================ const fn = (str) => { return Buffer.from(str).toString('base64'); }; module.exports = fn; ================================================ FILE: examples/05-api-directory/biz/say/hi.js ================================================ const reverse = require('../__lib/reverse'); const encode = require('./__lib/encode'); const fn = async (name, age) => { const msg = `Hi, I am ${name}, ${age} years old.`; const reversed = reverse(msg); const encoded = encode(msg); return {msg, reversed, encoded}; }; module.exports = fn; ================================================ FILE: examples/05-api-directory/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/06-with-database/biz/about.js ================================================ // Test // http://localhost:3000/about // Result // { // "success": true, // "data": { // "version": "1.0.0" // } // } const fn = async () => { return {version: '1.0.0'}; }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/get.js ================================================ // Test url // http://localhost:3000/user/get?username=owen const db = require('../../db'); const fn = async (username) => { return await db.user.select(username); }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/kill.js ================================================ // Test url // http://localhost:3000/user/kill?username=owen const db = require('../../db'); const fn = async (username) => { return await db.user.delete(username); }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/list.js ================================================ // Test url // http://localhost:3000/user/list const db = require('../../db'); const fn = async () => { return await db.user.select(); }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/login.js ================================================ // Test url // http://localhost:3000/user/login?username=owen&password=123 const db = require('../../db'); const fn = async (username, password) => { const result = await db.user.update({username, password}, {isOnline: 1}); return !!result; }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/logout.js ================================================ // Test url // http://localhost:3000/user/logout?username=owen const db = require('../../db'); const fn = async (username) => { const result = await db.user.update(username, {isOnline: 0}); return !!result; }; module.exports = fn; ================================================ FILE: examples/06-with-database/biz/user/register.js ================================================ // Test url // http://localhost:3000/user/register?username=owen&password=123 const db = require('../../db'); const fn = async (username, password) => { return await db.user.insert({username, password}); }; module.exports = fn; ================================================ FILE: examples/06-with-database/db/index.js ================================================ const data = { user: [ { id: 1, username: 'admin', password: '123456', isOnline: 0, } ] }; // Fake database const db = { user: { async insert(userInfo) { const id = data.user.length + 1; userInfo.isOnline = 0; userInfo.id = id; data.user.push(userInfo); return id; }, async update(where, filedValues) { const userInfo = await this.select(where); if (!userInfo) { return 0; } const keys = Object.keys(filedValues); keys.forEach(key => { userInfo[key] = filedValues[key]; }); return 1; }, async delete(where) { const userInfoId = await this.select(where, true); if (userInfoId) { data.user.splice(userInfoId - 1, 1); return 1; } return 0; }, async select(where, isReturnId) { if (!where) { return data.user; } else { if (typeof where === 'string') { where = {username: where}; } const whereKeys = Object.keys(where); const userInfo = data.user.find(item => { const index = whereKeys.findIndex(key => where[key] !== item[key]); if (index === -1) { return item; } }); if (userInfo) { return isReturnId ? userInfo.id : userInfo; } else { return null; } } } } }; module.exports = db; ================================================ FILE: examples/06-with-database/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/07-static-resources/biz/about.js ================================================ // Test // http://localhost:3000/about // Result // { // "success": true, // "data": { // "version": "1.0.0" // } // } const fn = async () => { return {version: '1.0.0'}; }; module.exports = fn; ================================================ FILE: examples/07-static-resources/index.js ================================================ require('../noapi')(); ================================================ FILE: examples/07-static-resources/public/images/1.jsx ================================================ // File type .jsx is not supported ================================================ FILE: examples/07-static-resources/public/index.html ================================================ Hello, world! ================================================ FILE: examples/07-static-resources/public/nav/index.html ================================================ /nav/index.html ================================================ FILE: examples/07-static-resources/public/nav/main.html ================================================ /nav/main.html ================================================ FILE: examples/99-options/config.js ================================================ module.exports = { name: 'myApi', dir: './src', host: '127.0.0.1', port: 3001, isSilence: true, }; ================================================ FILE: examples/99-options/index.js ================================================ const noapi = require('../noapi'); const config = require('./config'); noapi(config); ================================================ FILE: examples/99-options/src/about.js ================================================ // Test // http://localhost:3001/about // Result // { // "success": true, // "data": { // "version": "1.0.0" // } // } const fn = async () => { return {version: '1.0.0'}; }; module.exports = fn; ================================================ FILE: examples/noapi.js ================================================ module.exports = require('..'); ================================================ FILE: index.js ================================================ module.exports = require('./src'); ================================================ FILE: package.json ================================================ { "name": "noapi", "version": "2.2.2", "description": "A high-performance and easy-to-use web API framework for Node.js. Noapi loads directory \"biz\" as a web API server, each file in it defines and handles an API, so that you can focus on writing business function code. Noapi is simple enough that you just take it \"out of the box\".", "main": "index.js", "scripts": { "test": "node test" }, "keywords": [ "node.js", "api", "microservices", "microservices framework" ], "author": "hi.owen.luke@gmail.com", "license": "MIT", "dependencies": { "caller": "^1.0.1", "kdo": "^1.8.1", "keypaths": "^0.1.4", "lodash": "^4.17.15", "qs": "^6.9.1" }, "devDependencies": { "testor": "^0.6.5" }, "repository": { "type": "git", "url": "https://github.com/hiowenluke/noapi.git" }, "bugs": { "url": "https://github.com/hiowenluke/noapi/issues" }, "homepage": "https://github.com/hiowenluke/noapi#readme" } ================================================ FILE: src/biz/do.js ================================================ const paramsCache = require('./paramsCache'); const data = require('../data'); const keyPaths = require('keypaths'); const fn = async (api, query) => { const apiX = data.apisX[api]; // "/say/hi" => "say.hi" const func = keyPaths.get(data.handlers, apiX); // say.hi() if (typeof func !== 'function') { return {error: `The handler ./biz${api}.js does not exists.`}; } const names = paramsCache.getByApi(api); const args = names.map(name => query[name]); const result = await func(...args); return result; }; module.exports = fn; ================================================ FILE: src/biz/index.js ================================================ const paramsCache = require('./paramsCache'); const me = { init() { paramsCache.init(); } }; module.exports = me; ================================================ FILE: src/biz/paramsCache.js ================================================ const fs = require('fs'); const data = require('../data'); const config = require('../config'); const parseParamsNames = (str) => { // "(a, b = '', c) => {}" const temp = str.match(/\((.*?)\)/); // "a, b = '', c" if (!temp) return null; const params = temp[1].split(','); // ['a', " b = ''", ' c'] const names = params.map(item => item.replace(/(^\s*?(?=\b))|(\s*?$)/g, '').split('=')[0]); // ['a', 'b', 'c'] return names; }; const me = { data: {}, init() { const root = config.webServiceRoot; const bizDir = config.dir.replace(/^./, ''); // ./biz => /biz const apis = data.apis; apis.forEach(api => { let filePath = root + bizDir + api; // The filePath will be exists if it is a directory if (!fs.existsSync(filePath)) { // If it it not exists, then check for .js file filePath += '.js'; // If not found, then ignore it if (!fs.existsSync(filePath)) return; } // Require the directory or js file const bizFile = require(filePath); const content = bizFile.toString(); // "(a, b = '', c) => {}" const names = parseParamsNames(content); this.data[api] = names; }); }, getByApi(api) { return this.data[api] || []; } }; module.exports = me; ================================================ FILE: src/config.js ================================================ const fs = require('fs'); const path = require('path'); const me = { name: 'default', dir: './biz', host: 'localhost', port: 3000, isSilence: false, // Do not print logs if it is true // 0 print "Internal Server Error" // 1 print error message and stack 1 // 2 print full error stack onerror: 1, webServiceRoot: '', // The root path of web service enablePublic: '', // Enable public if the directory "public" is exists init(pathToCaller, args = []) { this.webServiceRoot = path.resolve(pathToCaller, '..'); this.enablePublic = fs.existsSync(this.webServiceRoot + '/public'); let name, dir, host, port, isSilence; if (typeof args[0] === 'object') { ({name, dir, host, port, isSilence} = args[0]); } else { args.forEach(arg => { if (!arg) return; const type = typeof arg; if (type === 'number') { port = arg; } else if (type === 'boolean') { isSilence = arg; } else if (type === 'string') { if (arg.substr(0, 1) === '.') { dir = arg; } else if (arg !== 'localhost' && arg.indexOf('.') === -1) { name = arg; } else { host = arg; } } }); } name && (this.name = name); dir && (this.dir = dir); host && (this.host = host); port && (this.port = port); isSilence && (this.isSilence = isSilence); }, }; module.exports = me; ================================================ FILE: src/data.js ================================================ const fs = require('fs'); const path = require('path'); const kdo = require('kdo'); const config = require('./config'); const keyPaths = require('keypaths'); const loadFolder = (folderName) => { let obj; const root = config.webServiceRoot; const folderPath = path.resolve(root + '/' + folderName); if (fs.existsSync(folderPath)) { const indexJs = folderPath + '/index.js'; if (fs.existsSync(indexJs)) { obj = require(folderPath); } else { const simulateIndexJs = {filename: indexJs}; obj = kdo(simulateIndexJs); } } return obj; }; const loadFolders = () => { const core = {}; const bizDirName = config.dir.replace(/^.\//, ''); // ./biz => biz core.api = loadFolder('api'); core.biz = loadFolder(bizDirName); const obj = core.api || core.biz || {}; const apiPaths = keyPaths.toPaths(obj); // ["say.hi"] // "say.hi" => "/say/hi" const apis = apiPaths.map(item => '/' + item.replace(/\./g, '/')); const apisX = {}; // {"/say/hi": "say.hi"} apis.forEach((api, index) => {apisX[api] = apiPaths[index]}); return {apis, apisX, handlers: core.biz}; }; const me = { apis: [], // ["/say/hi"] aha: {}, // {"/say/hi": "say.hi"} handlers: {}, // {say: {hi: f()}} init() { const {apis, apisX, handlers} = loadFolders(); this.apis = apis; this.apisX = apisX; this.handlers = handlers; }, }; module.exports = me; ================================================ FILE: src/index.js ================================================ const caller = require('caller'); const config = require('./config'); const data = require('./data'); const biz = require('./biz'); const server = require('./server'); const fn = (...args) => { config.init(caller(), args); data.init(); biz.init(); server.start(); }; module.exports = fn; ================================================ FILE: src/server/cross.js ================================================ const fn = (res) => { // Website you wish to allow to connect res.setHeader('Access-Control-Allow-Origin', '*'); // Request methods you wish to allow res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // Request headers you wish to allow res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); // Set to true if you need the website to include cookies in the requests sent // to the API (e.g. in case you use sessions) res.setHeader('Access-Control-Allow-Credentials', true); }; module.exports = fn; ================================================ FILE: src/server/index.js ================================================ const http = require('http'); const config = require('../config'); const routes = require('./routes'); const processPublic = require('./public'); const done = (res, result) => { if (typeof result === 'undefined') { result = {success: false, error: "Internal Server Error"}; } else if (result && result.error) { result = {success: false, error: result.error}; } else { result = {success: true, data: result}; } write(res, 'json', JSON.stringify(result)); }; const write = (res, type, str) => { const types = { json: 'application/json', html: 'text/html', }; if (type === 'html' && !/^/i.test(str)) { str = `
${str}`; } res.writeHead(200, {'Content-Type': types[type], 'X-Powered-By': 'Noapi'}); res.write(str); res.end(); }; const getQueryStr = (req) => { return new Promise(resolve => { let str = ''; req.on('data', (chunk) => { str += chunk; }); req.on('end',()=>{ resolve(str); }); }) }; const getErrorMessages = (arr) => { const messages = []; arr.forEach(a => { if (a.substr(0, 7 ) !== ' at ') { messages.push(a); } }); return messages; }; const me = { start() { const server = new http.Server(); server.on('request',async (req, res) => { const url = req.url; let [api, queryStr] = url.split('?'); if (config.enablePublic) { const done = processPublic(api, res); if (done) return; } else { if (api === '/favicon.ico') { return done(res, null); } if (api === '/') { return done(res, 'Welcome to Noapi.'); } } const method = req.method.toLowerCase(); if (method === 'post') { queryStr = await getQueryStr(req); } try { let result = await routes(api, queryStr); if (result && result.error) { !config.isSilence && console.log(result.error); } done(res, result); } catch(e) { const onerror = config.onerror; !config.isSilence && console.log(e); if (onerror === 0) { done(res); } else { const arr = e.stack.split('\n'); // print error message with stack 1 if (onerror === 1) { const messages = getErrorMessages(arr); const message = messages.join('\n'); let script = arr.find(a => a.substr(0, 7) === ' at '); script = script .replace(' at fn (', '') .replace(config.webServiceRoot, '.') .replace(/\)$/, '') ; done(res, {error: {message, script}}); } else { // print full error stack let str = arr.join('