From 7abd8e14375e2bd523fa398095317f900e9bdfc0 Mon Sep 17 00:00:00 2001 From: Alexandre Levacher Date: Fri, 31 Jan 2020 10:32:25 +0100 Subject: [PATCH 1/3] :recycle: Rewriting Lumie with typescript --- .eslintrc.json | 63 ++++-- .gitignore | 2 + example/app.js | 32 ---- example/controllers/cars/cars-get.action.js | 15 -- example/controllers/cars/cars-post.action.js | 13 -- example/controllers/cars/cars.routing.js | 22 --- example/controllers/cars/cars.spec.js | 32 ---- example/controllers/index.js | 14 -- example/controllers/index.spec.js | 12 -- example/controllers/simple-ctrl.js | 23 --- example/controllers/simple-ctrl.spec.js | 21 -- example/controllers/users/users.action.js | 17 -- example/controllers/users/users.routing.js | 20 -- example/controllers/users/users.spec.js | 32 ---- example/package.json | 34 ---- example/permissions.js | 6 - package.json | 52 ++--- src/helpers.js | 12 -- src/index.js | 98 ---------- src/logger.js | 6 - src/lumie.ts | 180 ++++++++++++++++++ src/route.js | 31 --- src/validators.js | 54 ------ test/browsing.spec.ts | 91 +++++++++ test/helpers.spec.js | 23 --- test/helpers.spec.ts | 40 ++++ test/load.spec.js | 11 -- test/loadRoute.spec.ts | 65 +++++++ test/logger.spec.js | 17 -- .../admin/users/users.notrouting.ts | 9 + .../admin/users/users.routing.ts | 10 + test/mockDirectory/index/index.routing.ts | 17 ++ test/mockDirectory/users/users.routing.ts | 20 ++ test/route.spec.js | 57 ------ test/validators.spec.js | 102 ---------- tsconfig.json | 65 +++++++ 36 files changed, 574 insertions(+), 744 deletions(-) delete mode 100644 example/app.js delete mode 100644 example/controllers/cars/cars-get.action.js delete mode 100644 example/controllers/cars/cars-post.action.js delete mode 100644 example/controllers/cars/cars.routing.js delete mode 100644 example/controllers/cars/cars.spec.js delete mode 100644 example/controllers/index.js delete mode 100644 example/controllers/index.spec.js delete mode 100644 example/controllers/simple-ctrl.js delete mode 100644 example/controllers/simple-ctrl.spec.js delete mode 100644 example/controllers/users/users.action.js delete mode 100644 example/controllers/users/users.routing.js delete mode 100644 example/controllers/users/users.spec.js delete mode 100644 example/package.json delete mode 100644 example/permissions.js delete mode 100644 src/helpers.js delete mode 100644 src/index.js delete mode 100644 src/logger.js create mode 100644 src/lumie.ts delete mode 100644 src/route.js delete mode 100644 src/validators.js create mode 100644 test/browsing.spec.ts delete mode 100644 test/helpers.spec.js create mode 100644 test/helpers.spec.ts delete mode 100644 test/load.spec.js create mode 100644 test/loadRoute.spec.ts delete mode 100644 test/logger.spec.js create mode 100644 test/mockDirectory/admin/users/users.notrouting.ts create mode 100644 test/mockDirectory/admin/users/users.routing.ts create mode 100644 test/mockDirectory/index/index.routing.ts create mode 100644 test/mockDirectory/users/users.routing.ts delete mode 100644 test/route.spec.js delete mode 100644 test/validators.spec.js create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 118c37c..bdd0d7f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,24 +1,53 @@ { - "extends": "airbnb-base", + "root": true, + "env": { + "node": true, + "es6": true + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [".ts"] + } + } + }, + "parser": "@typescript-eslint/parser", "plugins": [ - "import" + "import", + "@typescript-eslint" + ], + "extends": [ + "airbnb-base", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:import/typescript" ], "rules": { - "comma-dangle": ["error", { - "arrays": "never", - "objects": "never", - "imports": "always", - "exports": "always", - "functions": "ignore" - }], - "func-names": "off", - "guard-for-in": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/indent": ["error", 4, { "MemberExpression": 0 }], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-var-requires": "off", + "arrow-parens": ["error","as-needed"], + "camelcase": "off", + "global-require": "off", + "import/extensions": "off", "import/no-dynamic-require": "off", + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": "off", + "import/prefer-default-export": "off", "indent": ["error", 4, { "MemberExpression": 0 }], - "no-param-reassign": ["error", { - "props": false - }], - "no-restricted-syntax": "off", - "no-underscore-dangle": "off" + "max-len": ["error", { "code": 120 }], + "no-console": "warn", + "no-empty-function": "off", + "no-nested-ternary": "off", + "no-underscore-dangle": "off", + "no-useless-constructor": "off", + "object-curly-newline": "off", + "semi": "error" } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 99c6a30..25543b0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ package-lock.json # Core commiter folders Labo documentation + +dist \ No newline at end of file diff --git a/example/app.js b/example/app.js deleted file mode 100644 index e0d5e58..0000000 --- a/example/app.js +++ /dev/null @@ -1,32 +0,0 @@ -const express = require('express'); -const path = require('path'); -const Lumie = require('../src'); -const permissions = require('./permissions'); - -const app = express(); - -/** -** When the following code is commented, the route: /api/minimal/number/:value -** should return a 401 error because a user is not logged -* */ -// app.use((req, res, next) => { -// req.user = {}; -// next(); -// }); - -Lumie.load(app, { - verbose: process.env.NODE_ENV !== 'test', - preURL: 'api', - ignore: ['*.spec', '*.action'], - permissions, - controllers_path: path.join(__dirname, 'controllers') -}); - -const server = app.listen(3000, '127.0.0.1', () => { - const { address, port } = server.address(); - if (process.env.NODE_ENV !== 'test') { - console.log('Example app listening at http://%s:%s', address, port); /* eslint no-console: 0 */ - } -}); - -module.exports = app; diff --git a/example/controllers/cars/cars-get.action.js b/example/controllers/cars/cars-get.action.js deleted file mode 100644 index 3e4120b..0000000 --- a/example/controllers/cars/cars-get.action.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -* export Actions -*/ -module.exports.getAll = (req, res) => { - res.status(200).json([ - { brand: 'lamborghini', model: 'Huracan' }, - { brand: 'Tesla', model: 'S P100D' } - ]); -}; - -module.exports.getOne = (req, res) => { - res.status(200).json({ - brand: 'lamborghini', model: 'Huracan' - }); -}; diff --git a/example/controllers/cars/cars-post.action.js b/example/controllers/cars/cars-post.action.js deleted file mode 100644 index 50cdc4d..0000000 --- a/example/controllers/cars/cars-post.action.js +++ /dev/null @@ -1,13 +0,0 @@ -/** -* export Middlewares -*/ -module.exports.middlewares = [ - // Insert your middlewares here -]; - -/** -* export Action -*/ -module.exports.action = (req, res) => { - res.status(200).json({ msg: 'Your car have been created' }); -}; diff --git a/example/controllers/cars/cars.routing.js b/example/controllers/cars/cars.routing.js deleted file mode 100644 index 471720f..0000000 --- a/example/controllers/cars/cars.routing.js +++ /dev/null @@ -1,22 +0,0 @@ -const postCars = require('./cars-post.action'); -const getCars = require('./cars-get.action'); - -module.exports = { - '/': { - post: { - middlewares: postCars.middlewares, - action: postCars.action, - level: 'public' - }, - get: { - action: getCars.getAll, - level: 'public' - } - }, - '/:id': { - get: { - action: getCars.getOne, - level: 'public' - } - } -}; diff --git a/example/controllers/cars/cars.spec.js b/example/controllers/cars/cars.spec.js deleted file mode 100644 index 48d27e7..0000000 --- a/example/controllers/cars/cars.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -const test = require('ava'); -const request = require('supertest'); -const app = require('../../app'); - -test('[POST] /car should pass', async (t) => { - const { body, status } = await request(app) - .post('/api/cars') - .send({}) - .set('Accept', 'application/json'); - - t.is(body.msg, 'Your car have been created'); - t.is(status, 200); -}); - -test('[GET] /car should pass', async (t) => { - const { body, status } = await request(app) - .get('/api/cars') - .set('Accept', 'application/json'); - - t.is(body.length, 2); - t.is(status, 200); -}); - -test('[GET] /car/:id should pass', async (t) => { - const { body, status } = await request(app) - .get('/api/cars/lambo') - .set('Accept', 'application/json'); - - t.is(body.brand, 'lamborghini'); - t.is(body.model, 'Huracan'); - t.is(status, 200); -}); diff --git a/example/controllers/index.js b/example/controllers/index.js deleted file mode 100644 index 48f8014..0000000 --- a/example/controllers/index.js +++ /dev/null @@ -1,14 +0,0 @@ - -const index = (req, res) => { - res.json({ msg: 'I am an example of route with the path option' }); -}; - -module.exports = { - path: '/', - '/': { - get: { - action: index, - level: 'public' - } - } -}; diff --git a/example/controllers/index.spec.js b/example/controllers/index.spec.js deleted file mode 100644 index 9c47cda..0000000 --- a/example/controllers/index.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -const test = require('ava'); -const request = require('supertest'); -const app = require('../app'); - -test('[GET] / pass', async (t) => { - const { body, status } = await request(app) - .get('/api/') - .set('Accept', 'application/json'); - - t.is(status, 200); - t.is(body.msg, 'I am an example of route with the path option'); -}); diff --git a/example/controllers/simple-ctrl.js b/example/controllers/simple-ctrl.js deleted file mode 100644 index dd80b97..0000000 --- a/example/controllers/simple-ctrl.js +++ /dev/null @@ -1,23 +0,0 @@ - -const index = (req, res) => { - res.json({ msg: 'I am a minimal implementation' }); -}; - -const number = (req, res) => { - res.json({ msg: `The number is ${req.params.value}` }); -}; - -module.exports = { - '/': { - get: { - action: index, - level: 'public' - } - }, - '/number/:value': { - get: { - action: number, - level: 'member' - } - } -}; diff --git a/example/controllers/simple-ctrl.spec.js b/example/controllers/simple-ctrl.spec.js deleted file mode 100644 index a8d3e1b..0000000 --- a/example/controllers/simple-ctrl.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -const test = require('ava'); -const request = require('supertest'); -const app = require('../app'); - -test('[GET] /simple-ctrl should pass', async (t) => { - const { body, status } = await request(app) - .get('/api/simple-ctrl') - .set('Accept', 'application/json'); - - t.is(status, 200); - t.is(body.msg, 'I am a minimal implementation'); -}); - -test('[GET] /simple-ctrl/number/12 should return 401', async (t) => { - const { body, status } = await request(app) - .get('/api/simple-ctrl/number/12') - .set('Accept', 'application/json'); - - t.is(status, 401); - t.falsy(body.msg); -}); diff --git a/example/controllers/users/users.action.js b/example/controllers/users/users.action.js deleted file mode 100644 index 63ca760..0000000 --- a/example/controllers/users/users.action.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports.create = (req, res) => { - res.status(200).json({ msg: 'The User have been created' }); -}; - -module.exports.getOne = (req, res) => { - res.status(200).json({ - name: 'Alex', age: '24' - }); -}; - -module.exports.getAll = (req, res) => { - res.status(200).json([ - { name: 'Alex', age: '24' }, - { name: 'Flow', age: '24' }, - { name: 'Auré', age: '25' } - ]); -}; diff --git a/example/controllers/users/users.routing.js b/example/controllers/users/users.routing.js deleted file mode 100644 index 1d87c3a..0000000 --- a/example/controllers/users/users.routing.js +++ /dev/null @@ -1,20 +0,0 @@ -const { create, getOne, getAll } = require('./users.action'); - -module.exports = { - '/': { - post: { - action: create, - level: 'public' - }, - get: { - action: getAll, - level: 'public' - } - }, - '/:id': { - get: { - action: getOne, - level: 'public' - } - } -}; diff --git a/example/controllers/users/users.spec.js b/example/controllers/users/users.spec.js deleted file mode 100644 index 17fda46..0000000 --- a/example/controllers/users/users.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -const test = require('ava'); -const request = require('supertest'); -const app = require('../../app'); - -test('[POST] /user should pass', async (t) => { - const { body, status } = await request(app) - .post('/api/users') - .send({}) - .set('Accept', 'application/json'); - - t.is(body.msg, 'The User have been created'); - t.is(status, 200); -}); - -test('[GET] /user should pass', async (t) => { - const { body, status } = await request(app) - .get('/api/users') - .set('Accept', 'application/json'); - - t.is(body.length, 3); - t.is(status, 200); -}); - -test('[GET] /user/:id should pass', async (t) => { - const { body, status } = await request(app) - .get('/api/users/1') - .set('Accept', 'application/json'); - - t.is(body.name, 'Alex'); - t.is(body.age, '24'); - t.is(status, 200); -}); diff --git a/example/package.json b/example/package.json deleted file mode 100644 index d8b6365..0000000 --- a/example/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "example", - "version": "1.0.0", - "description": "", - "main": "app.js", - "scripts": { - "test": "cross-env NODE_ENV='test' nyc ava", - "start": "node app.js", - "ava": "ava" - }, - "author": "Alex Levacher", - "license": "ISC", - "dependencies": { - "express": "^4.16.3" - }, - "devDependencies": { - "ava": "^0.25.0", - "cross-env": "^5.1.4", - "eslint": "^4.19.1", - "eslint-plugin-import": "^2.11.0", - "nyc": "^11.7.1", - "supertest": "^3.0.0" - }, - "ava": { - "files": [ - "controllers/**/*.spec.js" - ], - "source": [ - "controllers/**/*.js" - ], - "verbose": true, - "concurrency": 1 - } -} diff --git a/example/permissions.js b/example/permissions.js deleted file mode 100644 index a25d1a2..0000000 --- a/example/permissions.js +++ /dev/null @@ -1,6 +0,0 @@ -const levelFcts = { - public: (req, res, next) => next(), - member: (req, res, next) => (req.user ? next() : res.sendStatus(401)) -}; - -module.exports = level => (req, res, next) => levelFcts[level](req, res, next); diff --git a/package.json b/package.json index 678a671..37c8bc5 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,9 @@ "description": "Make a dazzling node API", "main": "src/index.js", "scripts": { + "test": "ava", "start": "npm start --prefix example", - "test:src": "nyc ava -v", - "test:example": "npm test --prefix example", - "test": "npm run test:src && npm run test:example", - "lint": "eslint src/", - "ava": "ava" + "lint": "eslint src/" }, "repository": { "type": "git", @@ -30,30 +27,39 @@ }, "license": "MIT", "dependencies": { - "micromatch": "^3.1.10" + "chalk": "^3.0.0", + "lodash": "^4.17.15", + "micromatch": "^4.0.2" }, "devDependencies": { - "ava": "^0.25.0", - "eslint": "^4.19.1", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-plugin-import": "^2.11.0", - "nyc": "^12.0.2", - "pre-commit": "^1.2.2", - "sinon": "^6.0.0" + "@types/express": "^4.17.2", + "@types/lodash": "^4.14.149", + "@types/micromatch": "^4.0.0", + "@types/sinon": "^7.5.1", + "@typescript-eslint/eslint-plugin": "^2.17.0", + "@typescript-eslint/parser": "^2.17.0", + "ava": "^3.1.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.20.0", + "eslint": "^6.8.0", + "sinon": "^8.1.1", + "ts-node": "^8.6.2", + "typescript": "^3.7.5" }, "ava": { "files": [ - "test/**/*.spec.js" + "test/**/*.spec.ts" ], "source": [ - "src/**/*.js" + "src/**/*.ts" + ], + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" ], "verbose": true, - "concurrency": 1 - }, - "pre-commit": [ - "lint", - "test:src", - "test:example" - ] -} \ No newline at end of file + "concurrency": 2 + } +} diff --git a/src/helpers.js b/src/helpers.js deleted file mode 100644 index 2f747f2..0000000 --- a/src/helpers.js +++ /dev/null @@ -1,12 +0,0 @@ -function joinPathSlash(path) { - return path.replace(/\\/g, '/'); -} - -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -module.exports = { - joinPathSlash, - capitalize -}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 657019c..0000000 --- a/src/index.js +++ /dev/null @@ -1,98 +0,0 @@ -const fs = require('fs'); -const cpath = require('path'); -const mm = require('micromatch'); - -const Route = require('./route'); -const logger = require('./logger'); -const validators = require('./validators'); -const { capitalize } = require('./helpers'); - -const _routes = {}; -const _options = {}; - -function browseControllerObject(ctrlDefinition, path) { - const keyName = path.split('/') - .filter(item => (item && item !== _options.preURL)) - .map(item => capitalize(item)) - .join(' > '); - - _routes[keyName] = {}; - if (ctrlDefinition.path) delete ctrlDefinition.path; - for (const actionName in ctrlDefinition) { - const action = ctrlDefinition[actionName]; - for (const verbName in action) { - const routeID = cpath.join(verbName, path, actionName); - _routes[keyName][routeID] = new Route({ - verb: verbName, - action: action[verbName].action, - level: action[verbName].level, - path: cpath.join(path, actionName), - permissions: _options.permissions, - middlewares: action[verbName].middlewares - }); - } - } -} - -function browseDirectory(filePath, urlPath) { - fs.readdirSync(filePath) - .forEach((file) => { - const curtFilePath = cpath.join(filePath, file); - const stats = fs.statSync(curtFilePath); - if (stats.isDirectory()) { - return browseDirectory(curtFilePath, cpath.join(urlPath, file)); - } - let realCtrlName = file.substr(0, file.length - 3); - if (_options.ignore && _options.ignore.length) { - const match = mm.isMatch(realCtrlName, _options.ignore); - if (match) return false; - } - - /* eslint global-require: 0 */ - const controller = require(cpath.join(filePath, realCtrlName)); - - if (_options.routingFiles && mm.isMatch(realCtrlName, _options.routingFiles)) { - realCtrlName = ''; - } - if (controller.path) { - realCtrlName = controller.path; - } - const path = cpath.join('/', _options.preURL, urlPath, realCtrlName); - return browseControllerObject(controller, path); - }); -} - -function generateRoutes(app) { - for (const i in _routes) { - logger.log(`\n[${i}]`); - for (const j in _routes[i]) { - logger.log(_routes[i][j].desc()); - _routes[i][j].create(app); - } - } -} - -module.exports.load = function (app, options = {}) { - if (!app) { - throw new Error('Expected an express app as first argument'); - } - - const initOptionsFcts = { - controllers_path: validators.ctrlsPath, - verbose: validators.verbose, - ignore: validators.ignore, - preURL: validators.preURL, - permissions: validators.permissions, - routing_files: validators.routingFiles - }; - - for (const key in initOptionsFcts) { - initOptionsFcts[key](options[key], _options); - } - - logger.verbose = _options.verbose; - logger.log('\n======== ROUTES ========'); - browseDirectory(cpath.join(_options.controllersPath), '/'); - generateRoutes(app); - logger.log('\n========================'); -}; diff --git a/src/logger.js b/src/logger.js deleted file mode 100644 index 05770f0..0000000 --- a/src/logger.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports.verbose = false; - -module.exports.log = (msg) => { - /* eslint no-console: 0 */ - if (this.verbose) console.log(msg); -}; diff --git a/src/lumie.ts b/src/lumie.ts new file mode 100644 index 0000000..d51742c --- /dev/null +++ b/src/lumie.ts @@ -0,0 +1,180 @@ +import { Application, RequestHandler } from 'express'; +import { isMatch } from 'micromatch'; +import { flatten, compact, forOwn, invoke, groupBy, capitalize, isUndefined } from 'lodash'; +import { join, resolve } from 'path'; +import { readdirSync, statSync } from 'fs'; +import chalk from 'chalk'; + +const ERR_MSG = { + EXPECTED_APP: 'Expected an express app as first argument', +}; + +export const normalizePathForWindows = (path: string) => path.replace(/\\/g, '/'); + +export interface LumieOptions { + verbose?: boolean; + directoryPath?: string; + routingFiles?: string; + logger?: Function; + version?: string; + context?: string; + permissionsHandler?: (level: string) => RequestHandler; +} + +export interface LumieConfig extends LumieOptions { + verbose: boolean; + directoryPath: string; + routingFiles: string; + logger: Function; +} + +export interface RoutingDefinition { + verb: string; + ressource: string; + parameter: string; + level?: string; + action: RequestHandler; + middlewares?: RequestHandler[]; + filenameWithoutExtention: string; +} + +interface RoutingFile { + ressource: string; + filenameWithoutExtention: string; + sourcePath: string; +} + +export function getFullEnpoint( + { context, version, ressource, parameter }: + { context?: string; version?: string; ressource: string; parameter: string }, +): string { + return normalizePathForWindows( + join('/', ...compact([context, version, ressource, parameter])), + ); +} + +export function loadRoute(app: Application, route: RoutingDefinition, config: LumieConfig) { + const endpoint = getFullEnpoint({ + context: config.context, + version: config.version, + ressource: route.ressource, + parameter: route.parameter, + }); + + const permissionsHandler = config.permissionsHandler && route.level + ? config.permissionsHandler(route.level) + : null; + const middlewares = compact(route.middlewares); + const compactedMiddlewares = compact([permissionsHandler, ...middlewares]); + + return invoke(app, route.verb, endpoint, compactedMiddlewares, route.action); +} + +export function browseControllerDirectory( + directoryPath: string, ressource: string, config: LumieConfig, +): RoutingFile[] { + return flatten( + compact( + readdirSync(directoryPath) + .map(file => { + const currentSourcePath = join(directoryPath, file); + const updatedRessource = join(ressource, file); + const stats = statSync(currentSourcePath); + if (stats.isDirectory()) { + return browseControllerDirectory(currentSourcePath, updatedRessource, config); + } + const filenameWithoutExtention = file.substr(0, file.length - 3); + const isRoutingFile = isMatch(filenameWithoutExtention, config.routingFiles); + const routingFile: RoutingFile = { + filenameWithoutExtention, + ressource, + sourcePath: currentSourcePath, + }; + return isRoutingFile ? routingFile : null; + }), + ), + ); +} + +export function browseRoutingFiles(routingFiles: RoutingFile[]): RoutingDefinition[] { + const routingDefinitions: RoutingDefinition[] = []; + + routingFiles.forEach(routingFile => { + const requiredRoutingFile = require(resolve(routingFile.sourcePath)); + const { rewriteRessource } = requiredRoutingFile; + + forOwn(requiredRoutingFile, (parameterDefinition, parameter) => { + if (parameter === 'rewriteRessource') return; + + forOwn(parameterDefinition, (verbDefinition, verb) => { + const routingDefinition: RoutingDefinition = { + parameter, + verb, + ressource: isUndefined(rewriteRessource) ? routingFile.ressource : rewriteRessource, + filenameWithoutExtention: routingFile.filenameWithoutExtention, + level: verbDefinition.level, + action: verbDefinition.action, + middlewares: verbDefinition.middlewares, + }; + routingDefinitions.push(routingDefinition); + }); + }); + }); + + return routingDefinitions; +} + +function logRoutingDefinitions(routingDefinitions: RoutingDefinition[], config: LumieConfig) { + config.logger('=== LUMIE ROUTING ==='); + const groupedRoutingDefinitions = groupBy(routingDefinitions, 'ressource'); + + const HTTPVerbsColors: any = { + post: chalk.green, + get: chalk.blueBright, + put: chalk.yellow, + delete: chalk.red, + }; + + forOwn(groupedRoutingDefinitions, (definitions, ressource) => { + const title = ressource + .split('/') + .filter(item => item) + .map(capitalize) + .join(' > ') || definitions[0].filenameWithoutExtention; + + config.logger(`\n${chalk.bgWhiteBright.bold.black(` ${title} `)}`); + definitions.forEach(definition => { + const endpoint = getFullEnpoint({ + context: config.context, + version: config.version, + ressource: definition.ressource, + parameter: definition.parameter, + }); + const verbDisplay = HTTPVerbsColors[definition.verb](definition.verb.toUpperCase()); + const endpointDisplay = chalk.underline(endpoint); + const startLineDisplay = `${chalk.bgWhiteBright.bold(' ')}`; + config.logger(`${startLineDisplay} ${verbDisplay}\t[${definition.level}]\t${endpointDisplay}`); + }); + }); + + config.logger('\n================'); +} + +export const load = (app: Application, options?: LumieOptions) => { + if (!app) throw new Error(ERR_MSG.EXPECTED_APP); + + const defaultConfig: LumieConfig = { + verbose: true, + directoryPath: 'controllers', + routingFiles: '*.routing', + logger: console.log, + }; + + const config: LumieConfig = { ...defaultConfig, ...options }; + const routingFiles = browseControllerDirectory(config.directoryPath, '/', config); + const routingDefinitions = browseRoutingFiles(routingFiles); + + routingDefinitions.forEach(route => loadRoute(app, route, config)); + + if (config.verbose) logRoutingDefinitions(routingDefinitions, config); +}; diff --git a/src/route.js b/src/route.js deleted file mode 100644 index 9722b40..0000000 --- a/src/route.js +++ /dev/null @@ -1,31 +0,0 @@ -const { joinPathSlash } = require('./helpers'); - -class Route { - constructor(options) { - this.verb = options.verb; - this.action = options.action; - this.level = options.level; - this.path = options.path; - this.permissions = options.permissions; - this.middlewares = options.middlewares || []; - } - - create(app) { - if (this.permissions) { - app[this.verb]( - joinPathSlash(this.path), - this.permissions(this.level), - this.middlewares, - this.action - ); - return; - } - app[this.verb](joinPathSlash(this.path), this.middlewares, this.action); - } - - desc() { - return `\t${this.level}\t${this.verb}\t[${joinPathSlash(this.path)}]`; - } -} - -module.exports = Route; diff --git a/src/validators.js b/src/validators.js deleted file mode 100644 index 091f117..0000000 --- a/src/validators.js +++ /dev/null @@ -1,54 +0,0 @@ -const fs = require('fs'); - -// REQUIRED -function ctrlsPath(value, options) { - if (!value || typeof value !== 'string') { - throw new Error('Expected a controllers path in options to be a string'); - } - if (!fs.existsSync(value)) throw new Error('The controller path is incorrect'); - options.controllersPath = value; -} - -function verbose(value = false, options) { - if (value && typeof value !== 'boolean') { - throw new Error('Expected verbose to be a boolean'); - } - options.verbose = value; -} - -function ignore(value, options) { - if (value && !Array.isArray(value)) { - throw new Error('Expected ignore to be an Array'); - } - options.ignore = value; -} - -function preURL(value = '/', options) { - if (value && typeof value !== 'string') { - throw new Error('Expected preURL to be a string'); - } - options.preURL = value; -} - -function permissions(value, options) { - if (value && typeof value !== 'function') { - throw new Error('Expected permissions to be a function'); - } - options.permissions = value; -} - -function routingFiles(value = '*.routing', options) { - if (value && typeof value !== 'string') { - throw new Error('Expected routingFiles to be a string'); - } - options.routingFiles = value; -} - -module.exports = { - ctrlsPath, - verbose, - ignore, - preURL, - permissions, - routingFiles -}; diff --git a/test/browsing.spec.ts b/test/browsing.spec.ts new file mode 100644 index 0000000..55b6053 --- /dev/null +++ b/test/browsing.spec.ts @@ -0,0 +1,91 @@ +import test from 'ava'; +import { Request, Response } from 'express'; +import { browseControllerDirectory, LumieConfig, browseRoutingFiles } from '../src/lumie'; + +export const actionMethod = (req: Request, res: Response) => res.json(); + +const mockRoutingFiles = [ + { + filenameWithoutExtention: 'users.routing', + ressource: '/admin/users', + sourcePath: 'test/mockDirectory/admin/users/users.routing.ts', + }, + { + filenameWithoutExtention: 'index.routing', + ressource: '/index', + sourcePath: 'test/mockDirectory/index/index.routing.ts', + }, + { + filenameWithoutExtention: 'users.routing', + ressource: '/users', + sourcePath: 'test/mockDirectory/users/users.routing.ts', + }, +]; + +test('browsing the mock directory should return right RoutingFile array', t => { + const config = { routingFiles: '*.routing' } as LumieConfig; + + const routingFiles = browseControllerDirectory('test/mockDirectory', '/', config); + t.deepEqual(routingFiles, mockRoutingFiles); +}); + +test('browsing the routing files should return the right routing definitions', t => { + const routingDefinitions = browseRoutingFiles(mockRoutingFiles); + const action = actionMethod; + t.deepEqual(routingDefinitions, [ + { + parameter: '/', + verb: 'get', + ressource: '/admin/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'get', + ressource: '', + filenameWithoutExtention: 'index.routing', + level: 'public', + action, + middlewares: undefined, + }, + { + parameter: '/:project(apple|banana)', + verb: 'get', + ressource: '', + filenameWithoutExtention: 'index.routing', + level: 'public', + action, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'post', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'get', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action, + middlewares: undefined, + }, + { + parameter: '/:id', + verb: 'get', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action, + middlewares: undefined, + }, + ]); +}); diff --git a/test/helpers.spec.js b/test/helpers.spec.js deleted file mode 100644 index 25b0a90..0000000 --- a/test/helpers.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -const test = require('ava'); - -const { joinPathSlash, capitalize } = require('../src/helpers'); - -test('capitalizeshould works with one word', (t) => { - const value = capitalize('string'); - t.is(value, 'String'); -}); - -test('capitalize should works with two word', (t) => { - const value = capitalize('string string'); - t.is(value, 'String string'); -}); - -test('joinPathSlash should works with windows paths', (t) => { - const value = joinPathSlash('\\test\\foo'); - t.is(value, '/test/foo'); -}); - -test('joinPathSlash should works with basic paths', (t) => { - const value = joinPathSlash('/test/foo'); - t.is(value, '/test/foo'); -}); diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts new file mode 100644 index 0000000..7030ecb --- /dev/null +++ b/test/helpers.spec.ts @@ -0,0 +1,40 @@ +import test from 'ava'; +import { normalizePathForWindows, getFullEnpoint } from '../src/lumie'; + +test('normalizePathForWindows should works with windows paths', t => { + const value = normalizePathForWindows('\\test\\foo'); + t.is(value, '/test/foo'); +}); + +test('normalizePathForWindows should works with basic paths', t => { + const value = normalizePathForWindows('/test/foo'); + t.is(value, '/test/foo'); +}); + +test('getFullEnpoint should pass with all parameters', t => { + const endpoint = getFullEnpoint({ + context: 'api', + version: 'v2', + ressource: 'user', + parameter: ':id', + }); + t.is(endpoint, '/api/v2/user/:id'); +}); + +test('getFullEnpoint should pass without context', t => { + const endpoint = getFullEnpoint({ + version: 'v2', + ressource: 'user', + parameter: ':id', + }); + t.is(endpoint, '/v2/user/:id'); +}); + +test('getFullEnpoint should pass without version', t => { + const endpoint = getFullEnpoint({ + context: 'api', + ressource: 'user', + parameter: ':id', + }); + t.is(endpoint, '/api/user/:id'); +}); diff --git a/test/load.spec.js b/test/load.spec.js deleted file mode 100644 index 6317be5..0000000 --- a/test/load.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -const test = require('ava'); - -const expressCtrl = require('../src'); - -test('should fail without express app', (t) => { - const error = t.throws(() => { - expressCtrl.load(); - }, Error); - - t.is(error.message, 'Expected an express app as first argument'); -}); diff --git a/test/loadRoute.spec.ts b/test/loadRoute.spec.ts new file mode 100644 index 0000000..bcaadfd --- /dev/null +++ b/test/loadRoute.spec.ts @@ -0,0 +1,65 @@ +import test from 'ava'; +import { assert, spy } from 'sinon'; +import { NextFunction, Request, Response } from 'express'; +import { loadRoute, RoutingDefinition, LumieConfig } from '../src/lumie'; + +test('load the simplest route should pass', t => { + const app: any = { get: spy() }; + const config = {} as LumieConfig; + const route: RoutingDefinition = { + verb: 'get', + ressource: 'users', + filenameWithoutExtention: 'users.routing', + parameter: '', + action: () => true, + }; + + loadRoute(app, route, config); + + assert.calledOnce(app.get); + assert.calledWith(app.get, '/users', [], route.action); + t.pass(); +}); + +test('load route with middlewares should pass', t => { + const app: any = { get: spy() }; + const simpleMiddleware = (req: Request, res: Response, next: NextFunction) => next(); + const config = { } as LumieConfig; + const route: RoutingDefinition = { + verb: 'get', + ressource: 'users', + filenameWithoutExtention: 'users.routing', + parameter: '', + middlewares: [simpleMiddleware], + action: () => true, + }; + + loadRoute(app, route, config); + + assert.calledOnce(app.get); + assert.calledWith(app.get, '/users', [simpleMiddleware], route.action); + t.pass(); +}); + + +test('load route with a permission handler should pass', t => { + const app: any = { get: spy() }; + const simpleMiddleware = (req: Request, res: Response, next: NextFunction) => next(); + const permissionsHandler = (level: string) => simpleMiddleware; + const config = { permissionsHandler } as LumieConfig; + const route: RoutingDefinition = { + verb: 'get', + ressource: 'users', + filenameWithoutExtention: 'users.routing', + parameter: '', + middlewares: [simpleMiddleware], + level: 'public', + action: () => true, + }; + + loadRoute(app, route, config); + + assert.calledOnce(app.get); + assert.calledWith(app.get, '/users', [simpleMiddleware, simpleMiddleware], route.action); + t.pass(); +}); diff --git a/test/logger.spec.js b/test/logger.spec.js deleted file mode 100644 index eccf1b2..0000000 --- a/test/logger.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -const test = require('ava'); -const sinon = require('sinon'); - -const logger = require('../src/logger'); - -console.log = sinon.spy(); /* eslint no-console: "off" */ - -test('logger.log should not be called', (t) => { - logger.log('Hello'); - t.is(console.log.notCalled, true); -}); - -test('logger.log should be called', (t) => { - logger.verbose = true; - logger.log('Hello'); - t.is(console.log.calledOnceWith('Hello'), true); -}); diff --git a/test/mockDirectory/admin/users/users.notrouting.ts b/test/mockDirectory/admin/users/users.notrouting.ts new file mode 100644 index 0000000..b795efa --- /dev/null +++ b/test/mockDirectory/admin/users/users.notrouting.ts @@ -0,0 +1,9 @@ + +export default { + '/': { + get: { + action: null, + level: 'public', + }, + }, +}; diff --git a/test/mockDirectory/admin/users/users.routing.ts b/test/mockDirectory/admin/users/users.routing.ts new file mode 100644 index 0000000..177b04b --- /dev/null +++ b/test/mockDirectory/admin/users/users.routing.ts @@ -0,0 +1,10 @@ +import { actionMethod } from '../../../browsing.spec'; + +export = { + '/': { + get: { + action: actionMethod, + level: 'public', + }, + }, +}; diff --git a/test/mockDirectory/index/index.routing.ts b/test/mockDirectory/index/index.routing.ts new file mode 100644 index 0000000..3ff6079 --- /dev/null +++ b/test/mockDirectory/index/index.routing.ts @@ -0,0 +1,17 @@ +import { actionMethod } from '../../browsing.spec'; + +export = { + rewriteRessource: '', + '/': { + get: { + action: actionMethod, + level: 'public', + }, + }, + '/:project(apple|banana)': { + get: { + action: actionMethod, + level: 'public', + }, + }, +}; diff --git a/test/mockDirectory/users/users.routing.ts b/test/mockDirectory/users/users.routing.ts new file mode 100644 index 0000000..e1ea50b --- /dev/null +++ b/test/mockDirectory/users/users.routing.ts @@ -0,0 +1,20 @@ +import { actionMethod } from '../../browsing.spec'; + +export = { + '/': { + post: { + action: actionMethod, + level: 'public', + }, + get: { + action: actionMethod, + level: 'public', + }, + }, + '/:id': { + get: { + action: actionMethod, + level: 'public', + }, + }, +}; diff --git a/test/route.spec.js b/test/route.spec.js deleted file mode 100644 index 2f0fcd7..0000000 --- a/test/route.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -const test = require('ava'); -const sinon = require('sinon'); - -const Route = require('../src/route'); - -test('create get route without permissions should work', (t) => { - const prepareRoute = { - verb: 'get', - action: () => true, - level: undefined, - path: '/test/', - permissions: false, - middlewares: undefined - }; - const route = new Route(prepareRoute); - const app = { - get: sinon.spy() - }; - route.create(app); - t.is(app.get.calledOnceWith(prepareRoute.path, [], prepareRoute.action), true); -}); - -test('create get route with permissions should work', (t) => { - const prepareRoute = { - verb: 'get', - action: () => true, - level: 'public', - path: '/test/', - permissions: level => level, - middlewares: undefined - }; - const route = new Route(prepareRoute); - const app = { - get: sinon.spy() - }; - route.create(app); - t.is(app.get.calledOnceWith( - prepareRoute.path, - prepareRoute.permissions('public'), - [], - prepareRoute.action - ), true); -}); - -test('desc should work', (t) => { - const prepareRoute = { - verb: 'get', - action: () => true, - level: 'public', - path: '/test/', - permissions: level => level, - middlewares: undefined - }; - const route = new Route(prepareRoute); - const desc = route.desc(); - t.is(desc, '\tpublic\tget\t[/test/]'); -}); diff --git a/test/validators.spec.js b/test/validators.spec.js deleted file mode 100644 index 823b41b..0000000 --- a/test/validators.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -const test = require('ava'); - -const validators = require('../src/validators'); - -test('ctrlsPath should fail without path', (t) => { - const error = t.throws(() => { - validators.ctrlsPath(); - }, Error); - - t.is(error.message, 'Expected a controllers path in options to be a string'); -}); - -test('ctrlsPath should fail with an incorrect path', (t) => { - const error = t.throws(() => { - validators.ctrlsPath('./fake'); - }, Error); - - t.is(error.message, 'The controller path is incorrect'); -}); - -test('ctrlsPath should pass', (t) => { - validators.ctrlsPath('./src', {}); - t.pass(); -}); - -test('verbose should fail with incorrect value', (t) => { - const error = t.throws(() => { - validators.verbose(() => false, {}); - }, Error); - - t.is(error.message, 'Expected verbose to be a boolean'); -}); - -test('verbose should pass', (t) => { - validators.verbose(true, {}); - t.pass(); -}); - -test('ignore should fail with incorrect value', (t) => { - const error = t.throws(() => { - validators.ignore('heyYo', {}); - }, Error); - - t.is(error.message, 'Expected ignore to be an Array'); -}); - -test('ignore should pass', (t) => { - validators.ignore([], {}); - t.pass(); -}); - -test('preURL should fail with incorrect value', (t) => { - const error = t.throws(() => { - validators.preURL([], {}); - }, Error); - - t.is(error.message, 'Expected preURL to be a string'); -}); - -test('preURL should pass', (t) => { - validators.preURL('v2', {}); - t.pass(); -}); - -test('preURL should be / when not defined', (t) => { - const options = {}; - validators.preURL(undefined, options); - t.is(options.preURL, '/'); -}); - -test('permissions should fail with incorrect value', (t) => { - const error = t.throws(() => { - validators.permissions([], {}); - }, Error); - - t.is(error.message, 'Expected permissions to be a function'); -}); - -test('permissions should pass', (t) => { - validators.permissions(() => true, {}); - t.pass(); -}); - -test('routingFiles should fail with incorrect value', (t) => { - const error = t.throws(() => { - validators.routingFiles([], {}); - }, Error); - - t.is(error.message, 'Expected routingFiles to be a string'); -}); - -test('routingFiles should use default value', (t) => { - const op = {}; - validators.routingFiles(undefined, op); - t.is(op.routingFiles, '*.routing'); -}); - -test('routingFiles should pass', (t) => { - const op = {}; - validators.routingFiles('*.routingBis', op); - t.is(op.routingFiles, '*.routingBis'); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fb4b353 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,65 @@ +{ + "compileOnSave": true, + "files": ["./src/lumie.ts"], + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + "typeRoots": ["./node_modules/@types"], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} From 8d4b351d9567910071622d8888fbddbbc29d86bf Mon Sep 17 00:00:00 2001 From: Alexandre Levacher Date: Fri, 31 Jan 2020 14:06:28 +0100 Subject: [PATCH 2/3] Improve testing --- src/lumie.ts | 6 +- test/browser.spec.ts | 17 ++++ test/browsing.spec.ts | 91 ------------------- test/{loadRoute.spec.ts => loader.spec.ts} | 0 test/logger.spec.ts | 38 ++++++++ test/mock/actionMethod.ts | 3 + .../admin/users/notARoutingFile.ts | 1 + .../controllers/admin/users/users.routing.ts | 10 ++ .../controllers}/index/index.routing.ts | 6 +- .../controllers}/users/users.routing.ts | 8 +- test/mock/expectedBrowsingResults.ts | 77 ++++++++++++++++ .../admin/users/users.notrouting.ts | 9 -- .../admin/users/users.routing.ts | 10 -- test/previewLoggerResults.ts | 5 + 14 files changed, 161 insertions(+), 120 deletions(-) create mode 100644 test/browser.spec.ts delete mode 100644 test/browsing.spec.ts rename test/{loadRoute.spec.ts => loader.spec.ts} (100%) create mode 100644 test/logger.spec.ts create mode 100644 test/mock/actionMethod.ts create mode 100644 test/mock/controllers/admin/users/notARoutingFile.ts create mode 100644 test/mock/controllers/admin/users/users.routing.ts rename test/{mockDirectory => mock/controllers}/index/index.routing.ts (60%) rename test/{mockDirectory => mock/controllers}/users/users.routing.ts (55%) create mode 100644 test/mock/expectedBrowsingResults.ts delete mode 100644 test/mockDirectory/admin/users/users.notrouting.ts delete mode 100644 test/mockDirectory/admin/users/users.routing.ts create mode 100644 test/previewLoggerResults.ts diff --git a/src/lumie.ts b/src/lumie.ts index d51742c..031ff61 100644 --- a/src/lumie.ts +++ b/src/lumie.ts @@ -25,7 +25,7 @@ export interface LumieConfig extends LumieOptions { verbose: boolean; directoryPath: string; routingFiles: string; - logger: Function; + logger: any; } export interface RoutingDefinition { @@ -38,7 +38,7 @@ export interface RoutingDefinition { filenameWithoutExtention: string; } -interface RoutingFile { +export interface RoutingFile { ressource: string; filenameWithoutExtention: string; sourcePath: string; @@ -124,7 +124,7 @@ export function browseRoutingFiles(routingFiles: RoutingFile[]): RoutingDefiniti return routingDefinitions; } -function logRoutingDefinitions(routingDefinitions: RoutingDefinition[], config: LumieConfig) { +export function logRoutingDefinitions(routingDefinitions: RoutingDefinition[], config: LumieConfig) { config.logger('=== LUMIE ROUTING ==='); const groupedRoutingDefinitions = groupBy(routingDefinitions, 'ressource'); diff --git a/test/browser.spec.ts b/test/browser.spec.ts new file mode 100644 index 0000000..17e6cbe --- /dev/null +++ b/test/browser.spec.ts @@ -0,0 +1,17 @@ +import test from 'ava'; +import { browseControllerDirectory, LumieConfig, browseRoutingFiles } from '../src/lumie'; +import { expectedMockRoutingFiles, expectedRoutingDefinitions } from './mock/expectedBrowsingResults'; + +const directoryPath = 'test/mock/controllers'; + +test('browsing the mock directory should return right RoutingFile array', t => { + const config = { routingFiles: '*.routing' } as LumieConfig; + + const routingFiles = browseControllerDirectory(directoryPath, '/', config); + t.deepEqual(routingFiles, expectedMockRoutingFiles); +}); + +test('browsing the routing files should return the right routing definitions', t => { + const routingDefinitions = browseRoutingFiles(expectedMockRoutingFiles); + t.deepEqual(routingDefinitions, expectedRoutingDefinitions); +}); diff --git a/test/browsing.spec.ts b/test/browsing.spec.ts deleted file mode 100644 index 55b6053..0000000 --- a/test/browsing.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import test from 'ava'; -import { Request, Response } from 'express'; -import { browseControllerDirectory, LumieConfig, browseRoutingFiles } from '../src/lumie'; - -export const actionMethod = (req: Request, res: Response) => res.json(); - -const mockRoutingFiles = [ - { - filenameWithoutExtention: 'users.routing', - ressource: '/admin/users', - sourcePath: 'test/mockDirectory/admin/users/users.routing.ts', - }, - { - filenameWithoutExtention: 'index.routing', - ressource: '/index', - sourcePath: 'test/mockDirectory/index/index.routing.ts', - }, - { - filenameWithoutExtention: 'users.routing', - ressource: '/users', - sourcePath: 'test/mockDirectory/users/users.routing.ts', - }, -]; - -test('browsing the mock directory should return right RoutingFile array', t => { - const config = { routingFiles: '*.routing' } as LumieConfig; - - const routingFiles = browseControllerDirectory('test/mockDirectory', '/', config); - t.deepEqual(routingFiles, mockRoutingFiles); -}); - -test('browsing the routing files should return the right routing definitions', t => { - const routingDefinitions = browseRoutingFiles(mockRoutingFiles); - const action = actionMethod; - t.deepEqual(routingDefinitions, [ - { - parameter: '/', - verb: 'get', - ressource: '/admin/users', - filenameWithoutExtention: 'users.routing', - level: 'public', - action, - middlewares: undefined, - }, - { - parameter: '/', - verb: 'get', - ressource: '', - filenameWithoutExtention: 'index.routing', - level: 'public', - action, - middlewares: undefined, - }, - { - parameter: '/:project(apple|banana)', - verb: 'get', - ressource: '', - filenameWithoutExtention: 'index.routing', - level: 'public', - action, - middlewares: undefined, - }, - { - parameter: '/', - verb: 'post', - ressource: '/users', - filenameWithoutExtention: 'users.routing', - level: 'public', - action, - middlewares: undefined, - }, - { - parameter: '/', - verb: 'get', - ressource: '/users', - filenameWithoutExtention: 'users.routing', - level: 'public', - action, - middlewares: undefined, - }, - { - parameter: '/:id', - verb: 'get', - ressource: '/users', - filenameWithoutExtention: 'users.routing', - level: 'public', - action, - middlewares: undefined, - }, - ]); -}); diff --git a/test/loadRoute.spec.ts b/test/loader.spec.ts similarity index 100% rename from test/loadRoute.spec.ts rename to test/loader.spec.ts diff --git a/test/logger.spec.ts b/test/logger.spec.ts new file mode 100644 index 0000000..ae88e36 --- /dev/null +++ b/test/logger.spec.ts @@ -0,0 +1,38 @@ +import test from 'ava'; +import { assert, spy } from 'sinon'; +import { logRoutingDefinitions, LumieConfig } from '../src/lumie'; +import { basicActionMethod } from './mock/actionMethod'; + +test('Logging simple routingDefinitions should pass', t => { + const config = { logger: spy() } as LumieConfig; + const routingDefinition = [ + { + parameter: '/', + verb: 'get', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/:id', + verb: 'get', + ressource: '/cars', + filenameWithoutExtention: 'index.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + ]; + + logRoutingDefinitions(routingDefinition, config); + assert.callCount(config.logger, 6); + assert.calledWith(config.logger, '=== LUMIE ROUTING ==='); + assert.calledWith(config.logger, '\n================'); + assert.calledWith(config.logger, '\n Users '); + assert.calledWith(config.logger, '\n Cars '); + assert.calledWith(config.logger, ' GET\t[public]\t/users/'); + assert.calledWith(config.logger, ' GET\t[public]\t/cars/:id'); + t.pass(); +}); diff --git a/test/mock/actionMethod.ts b/test/mock/actionMethod.ts new file mode 100644 index 0000000..20b845f --- /dev/null +++ b/test/mock/actionMethod.ts @@ -0,0 +1,3 @@ +import { Request, Response } from 'express'; + +export const basicActionMethod = (req: Request, res: Response) => res.json('OK'); diff --git a/test/mock/controllers/admin/users/notARoutingFile.ts b/test/mock/controllers/admin/users/notARoutingFile.ts new file mode 100644 index 0000000..0660cf4 --- /dev/null +++ b/test/mock/controllers/admin/users/notARoutingFile.ts @@ -0,0 +1 @@ +console.log("I'm not a routing file. I sould not be read by Lumie"); diff --git a/test/mock/controllers/admin/users/users.routing.ts b/test/mock/controllers/admin/users/users.routing.ts new file mode 100644 index 0000000..9e7b0a9 --- /dev/null +++ b/test/mock/controllers/admin/users/users.routing.ts @@ -0,0 +1,10 @@ +import { basicActionMethod } from '../../../actionMethod'; + +export = { + '/': { + get: { + action: basicActionMethod, + level: 'public', + }, + }, +}; diff --git a/test/mockDirectory/index/index.routing.ts b/test/mock/controllers/index/index.routing.ts similarity index 60% rename from test/mockDirectory/index/index.routing.ts rename to test/mock/controllers/index/index.routing.ts index 3ff6079..051970c 100644 --- a/test/mockDirectory/index/index.routing.ts +++ b/test/mock/controllers/index/index.routing.ts @@ -1,16 +1,16 @@ -import { actionMethod } from '../../browsing.spec'; +import { basicActionMethod } from '../../actionMethod'; export = { rewriteRessource: '', '/': { get: { - action: actionMethod, + action: basicActionMethod, level: 'public', }, }, '/:project(apple|banana)': { get: { - action: actionMethod, + action: basicActionMethod, level: 'public', }, }, diff --git a/test/mockDirectory/users/users.routing.ts b/test/mock/controllers/users/users.routing.ts similarity index 55% rename from test/mockDirectory/users/users.routing.ts rename to test/mock/controllers/users/users.routing.ts index e1ea50b..e474450 100644 --- a/test/mockDirectory/users/users.routing.ts +++ b/test/mock/controllers/users/users.routing.ts @@ -1,19 +1,19 @@ -import { actionMethod } from '../../browsing.spec'; +import { basicActionMethod } from '../../actionMethod'; export = { '/': { post: { - action: actionMethod, + action: basicActionMethod, level: 'public', }, get: { - action: actionMethod, + action: basicActionMethod, level: 'public', }, }, '/:id': { get: { - action: actionMethod, + action: basicActionMethod, level: 'public', }, }, diff --git a/test/mock/expectedBrowsingResults.ts b/test/mock/expectedBrowsingResults.ts new file mode 100644 index 0000000..b8b0ebf --- /dev/null +++ b/test/mock/expectedBrowsingResults.ts @@ -0,0 +1,77 @@ +import { basicActionMethod } from './actionMethod'; +import { RoutingDefinition, RoutingFile } from '../../src/lumie'; + +export const expectedMockRoutingFiles: RoutingFile[] = [ + { + filenameWithoutExtention: 'users.routing', + ressource: '/admin/users', + sourcePath: 'test/mock/controllers/admin/users/users.routing.ts', + }, + { + filenameWithoutExtention: 'index.routing', + ressource: '/index', + sourcePath: 'test/mock/controllers/index/index.routing.ts', + }, + { + filenameWithoutExtention: 'users.routing', + ressource: '/users', + sourcePath: 'test/mock/controllers/users/users.routing.ts', + }, +]; + +export const expectedRoutingDefinitions: RoutingDefinition[] = [ + { + parameter: '/', + verb: 'get', + ressource: '/admin/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'get', + ressource: '', + filenameWithoutExtention: 'index.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/:project(apple|banana)', + verb: 'get', + ressource: '', + filenameWithoutExtention: 'index.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'post', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/', + verb: 'get', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, + { + parameter: '/:id', + verb: 'get', + ressource: '/users', + filenameWithoutExtention: 'users.routing', + level: 'public', + action: basicActionMethod, + middlewares: undefined, + }, +]; diff --git a/test/mockDirectory/admin/users/users.notrouting.ts b/test/mockDirectory/admin/users/users.notrouting.ts deleted file mode 100644 index b795efa..0000000 --- a/test/mockDirectory/admin/users/users.notrouting.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export default { - '/': { - get: { - action: null, - level: 'public', - }, - }, -}; diff --git a/test/mockDirectory/admin/users/users.routing.ts b/test/mockDirectory/admin/users/users.routing.ts deleted file mode 100644 index 177b04b..0000000 --- a/test/mockDirectory/admin/users/users.routing.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { actionMethod } from '../../../browsing.spec'; - -export = { - '/': { - get: { - action: actionMethod, - level: 'public', - }, - }, -}; diff --git a/test/previewLoggerResults.ts b/test/previewLoggerResults.ts new file mode 100644 index 0000000..2ecb0da --- /dev/null +++ b/test/previewLoggerResults.ts @@ -0,0 +1,5 @@ +import { logRoutingDefinitions, LumieConfig } from '../src/lumie'; +import { expectedRoutingDefinitions } from './mock/expectedBrowsingResults'; + +// ts-node test/previewLoggerResults.ts +logRoutingDefinitions(expectedRoutingDefinitions, { logger: console.log as Function } as LumieConfig); From 18045850641254d732ab0305b8f916bc5a5f906f Mon Sep 17 00:00:00 2001 From: Alexandre Levacher Date: Fri, 31 Jan 2020 14:33:42 +0100 Subject: [PATCH 3/3] Clean tsconfig Clean tsconfig --- tsconfig.json | 72 +++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index fb4b353..862e093 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,65 +1,35 @@ { "compileOnSave": true, - "files": ["./src/lumie.ts"], + "files": [ + "./src/lumie.ts" + ], "compilerOptions": { /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "checkJs": true, /* Report errors in .js files. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "outDir": "./dist", + // "outFile": "./", /* Redirect output structure to the directory. */ + "removeComments": true, /* Do not emit comments to output. */ "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* Enable strict null checks. */ - "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + "strict": true, /* Enable all strict type-checking options. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictNullChecks": true, /* Enable strict null checks. */ /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["./node_modules/@types"], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "typeRoots": ["./node_modules/@types"] /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ } }