@trigo/fsm
Advanced tools
| 'use strict'; | ||
| function getNested(theObject, path) { | ||
| try { | ||
| const separator = '.'; | ||
| return path | ||
| .replace('[', separator).replace(']', '') | ||
| .split(separator) | ||
| .reduce( | ||
| (obj, property) => obj[property], theObject, | ||
| ); | ||
| } catch (err) { | ||
| return undefined; | ||
| } | ||
| } | ||
| const replaceFromParams = (path, params, ctx) => { | ||
| let href = path; | ||
| Object.keys(params).forEach((key) => { | ||
| const value = getNested(ctx, params[key]); | ||
| if (value === undefined) { | ||
| throw new Error(`Cannot resolve param: "${key}" data path: "${params[key]}"`); | ||
| } | ||
| href = href.replace(new RegExp(`{${key}}`, 'g'), value); | ||
| }); | ||
| return href; | ||
| }; | ||
| module.exports = ({ api, ctx }) => { | ||
| if (!api) throw new Error('Argument "api" missing'); | ||
| if (!api.path) throw new Error('Argument "api.path" missing'); | ||
| const method = api.method || 'get'; | ||
| let href = api.path; | ||
| if (ctx && ctx.api && ctx.api.params) { | ||
| href = replaceFromParams(href, ctx.api.params, ctx); | ||
| } | ||
| if (api.params) { | ||
| href = replaceFromParams(href, api.params, ctx); | ||
| } | ||
| return { | ||
| href, | ||
| method, | ||
| }; | ||
| }; |
| 'use strict'; | ||
| const parseApi = require('./parse-transition-api'); | ||
| const { expect } = require('chai'); | ||
| describe('parse transition api', () => { | ||
| it('throws with missing "api"', () => { | ||
| expect(() => parseApi({ })).to.throw('Argument "api" missing'); | ||
| }); | ||
| it('throws with missing "api.path"', () => { | ||
| expect(() => parseApi({ api: { method: 'patch' } })).to.throw('Argument "api.path" missing'); | ||
| }); | ||
| it('returns corrct object', () => { | ||
| const res = parseApi({ api: { | ||
| path: '/test', | ||
| method: 'patch', | ||
| } }); | ||
| expect(res).to.eql({ | ||
| href: '/test', | ||
| method: 'patch', | ||
| }); | ||
| }); | ||
| it('resolves params from ctx object', () => { | ||
| expect(parseApi({ | ||
| api: { | ||
| path: '/test/{resId}', | ||
| method: 'patch', | ||
| params: { | ||
| resId: 'data.event.resId', | ||
| }, | ||
| }, | ||
| ctx: { | ||
| data: { | ||
| event: { | ||
| resId: '42', | ||
| }, | ||
| }, | ||
| }, | ||
| })).to.eql({ | ||
| href: '/test/42', | ||
| method: 'patch', | ||
| }); | ||
| }); | ||
| it('throws error when param cannot be resolved', () => { | ||
| expect(() => parseApi({ | ||
| api: { | ||
| path: '/test/{resId}', | ||
| method: 'patch', | ||
| params: { | ||
| resId: 'data.event.resId', | ||
| }, | ||
| }, | ||
| ctx: { | ||
| data: { | ||
| event: { | ||
| }, | ||
| }, | ||
| }, | ||
| })).to.throw('Cannot resolve param: "resId" data path: "data.event.resId"'); | ||
| }); | ||
| }); |
+1
-1
@@ -1,1 +0,1 @@ | ||
| v7.6.0 | ||
| v8.1.4 |
+1
-1
@@ -1,2 +0,2 @@ | ||
| FROM trigo/node-base:7.6-yarn-lib | ||
| FROM trigo/node-base:8.1-yarn-lib | ||
+34
-3
@@ -11,2 +11,3 @@ 'use strict'; | ||
| const getAllTakenNames = require('./get-all-taken-names'); | ||
| const parseTrasitionApi = require('./parse-transition-api'); | ||
@@ -26,3 +27,2 @@ const callIfSet = async (handler, ctx, args) => { | ||
| class FSM { | ||
| /** | ||
@@ -61,3 +61,3 @@ * Returns the composit state tool used to parse and build state strings from objects | ||
| */ | ||
| constructor({ initialState, transitions, data, saveState, willChangeState, didChangeState, willSaveState, didSaveState, eventHandler }) { | ||
| constructor({ initialState, transitions, data, saveState, willChangeState, didChangeState, willSaveState, didSaveState, eventHandler, api }) { | ||
| this._state = '__uninitialized__'; | ||
@@ -73,2 +73,3 @@ this._transitions = []; | ||
| this._trasitionFunctionNames = []; | ||
| this._api = api || {}; | ||
@@ -139,2 +140,32 @@ if (transitions) { | ||
| /** | ||
| * Get rest API links for all currently available transitions than define the | ||
| * "restApi" object | ||
| */ | ||
| restApi() { | ||
| const api = {}; | ||
| if (this._api && this._api.self) { | ||
| api.self = parseTrasitionApi({ | ||
| api: this._api.self, | ||
| ctx: { | ||
| data: this._data, | ||
| api: this._api, | ||
| }, | ||
| }); | ||
| } | ||
| findPossibleTransitions(this.state, this._transitions) | ||
| .filter(t => t.api) | ||
| .forEach((t) => { | ||
| api[t.name] = parseTrasitionApi({ | ||
| api: t.api, | ||
| ctx: { | ||
| data: this._data, | ||
| api: this._api, | ||
| }, | ||
| }); | ||
| }); | ||
| return api; | ||
| } | ||
| /** | ||
| * Execute a transition | ||
@@ -244,3 +275,3 @@ * | ||
| result[beforeHandler] = await callIfSet(this._eventHandler[beforeHandler], ctx, args); | ||
| // console.log(`Change state: "${from}" => "${to}"`) | ||
| // console.log(`Change state: "${from}" => "${to}"`) | ||
| this._state = to; | ||
@@ -247,0 +278,0 @@ |
+144
-0
@@ -593,2 +593,146 @@ 'use strict'; | ||
| }); | ||
| describe('transition REST API', () => { | ||
| let fsm, cfg; | ||
| beforeEach(() => { | ||
| cfg = { | ||
| initialState: 'a', | ||
| transitions: [{ | ||
| name: 't1', | ||
| from: '*', | ||
| to: 'a', | ||
| api: { | ||
| path: '/entity/trans', | ||
| method: 'patch', | ||
| }, | ||
| }, { | ||
| name: 't2', | ||
| from: '*', | ||
| to: 'a', | ||
| api: { | ||
| path: '/entity/{resId}/{trans}/{subId}', | ||
| method: 'patch', | ||
| params: { | ||
| resId: 'data.resId', | ||
| subId: 'data._embedded.event.resId', | ||
| }, | ||
| }, | ||
| }, { | ||
| name: 't3', | ||
| from: '*', | ||
| to: 'a', | ||
| }, { | ||
| name: 't4', | ||
| from: '*', | ||
| to: 'a', | ||
| api: { | ||
| path: '/{entity}/{resId}/{trans}/{subId}', | ||
| method: 'patch', | ||
| params: { | ||
| resId: 'data.resId', | ||
| subId: 'data._embedded.event.resId', | ||
| }, | ||
| }, | ||
| }], | ||
| saveState: () => {}, | ||
| willChangeState: () => bb.delay(5), | ||
| data: { | ||
| resId: '42', | ||
| _embedded: { | ||
| event: { | ||
| resId: '22', | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| }); | ||
| it('exposes "restApi()" function', async () => { | ||
| fsm = new FSM(cfg); | ||
| expect(fsm.restApi).to.be.a('function'); | ||
| }); | ||
| it('filters transitions without "api" property', async () => { | ||
| fsm = new FSM(cfg); | ||
| const r = fsm.restApi(); | ||
| expect(r.t1).to.exist; | ||
| expect(r.t2).to.exist; | ||
| expect(r.t3).not.to.exist; | ||
| }); | ||
| it('returns parsed api object', async () => { | ||
| fsm = new FSM(cfg); | ||
| const r = fsm.restApi(); | ||
| expect(r.t1).to.eql({ | ||
| href: '/entity/trans', | ||
| method: 'patch', | ||
| }); | ||
| }); | ||
| it('returns "self" when declared', async () => { | ||
| fsm = new FSM(Object.assign({}, cfg, { | ||
| api: { | ||
| self: { | ||
| path: '/entity/{resId}', | ||
| }, | ||
| params: { | ||
| resId: 'data.resId', | ||
| }, | ||
| }, | ||
| })); | ||
| const r = fsm.restApi(); | ||
| expect(r.self).to.eql({ | ||
| href: '/entity/42', | ||
| method: 'get', | ||
| }); | ||
| }); | ||
| it('can mix global & transition local params in same route', () => { | ||
| fsm = new FSM(Object.assign({}, cfg, { | ||
| api: { | ||
| data: { | ||
| entity: 'events', | ||
| }, | ||
| self: { | ||
| path: '/{entity}/{resId}', | ||
| }, | ||
| params: { | ||
| entity: 'api.data.entity', | ||
| resId: 'data.resId', | ||
| }, | ||
| }, | ||
| })); | ||
| const r = fsm.restApi(); | ||
| expect(r.t4).to.eql({ | ||
| href: '/events/42/{trans}/22', | ||
| method: 'patch', | ||
| }); | ||
| }); | ||
| it('can declare static params data in "api.data" object', () => { | ||
| fsm = new FSM(Object.assign({}, cfg, { | ||
| api: { | ||
| data: { | ||
| entity: 'events', | ||
| }, | ||
| self: { | ||
| path: '/{entity}/{resId}', | ||
| }, | ||
| params: { | ||
| entity: 'api.data.entity', | ||
| resId: 'data.resId', | ||
| }, | ||
| }, | ||
| })); | ||
| const r = fsm.restApi(); | ||
| expect(r.self).to.eql({ | ||
| href: '/events/42', | ||
| method: 'get', | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
+8
-8
@@ -12,19 +12,19 @@ SHELL=/bin/bash | ||
| install: | ||
| yarn install | ||
| @yarn install | ||
| clean: | ||
| rm -rf node_modules/ | ||
| @rm -rf node_modules/ | ||
| test: | ||
| yarn test | ||
| @yarn test | ||
| .PHONY: docs | ||
| docs: | ||
| esdoc | ||
| @esdoc | ||
| build: . | ||
| docker-compose -f docker-compose.test.yml build | ||
| @docker-compose -f docker-compose.test.yml build | ||
| lint: | ||
| yarn lint | ||
| @yarn lint | ||
@@ -56,6 +56,6 @@ ci-lint: build | ||
| dev-inf-up: | ||
| docker-compose -f docker-compose.dev-inf.yml up -d | ||
| @docker-compose -f docker-compose.dev-inf.yml up -d | ||
| dev-inf-down: | ||
| docker-compose -f docker-compose.dev-inf.yml down | ||
| @docker-compose -f docker-compose.dev-inf.yml down | ||
| : |
+8
-8
| { | ||
| "name": "@trigo/fsm", | ||
| "version": "3.0.0", | ||
| "version": "3.0.1", | ||
| "description": "FSM - Finite State Machine", | ||
@@ -15,11 +15,11 @@ "main": "index.js", | ||
| "bluebird": "^3.5.0", | ||
| "chai": "^3.5.0", | ||
| "chai": "^4.0.2", | ||
| "esdoc": "^0.5.2", | ||
| "eslint": "^3.17.1", | ||
| "eslint-config-airbnb-base": "^11.1.1", | ||
| "eslint-plugin-import": "^2.2.0", | ||
| "eslint-plugin-mocha": "^4.9.0", | ||
| "mocha": "^3.2.0", | ||
| "eslint": "^4.1.1", | ||
| "eslint-config-airbnb-base": "^11.2.0", | ||
| "eslint-plugin-import": "^2.6.1", | ||
| "eslint-plugin-mocha": "^4.11.0", | ||
| "mocha": "^3.4.2", | ||
| "nodemon": "^1.11.0", | ||
| "nyc": "^10.2.0" | ||
| "nyc": "^11.0.3" | ||
| }, | ||
@@ -26,0 +26,0 @@ "nyc": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
11643802
0.08%96
2.13%188754
0.14%7
16.67%