diff --git a/.gitignore b/.gitignore index d500b94..ca0eaef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules +/generated-snapshots *.idea *.vscode diff --git a/readme.md b/readme.md index 1ef7a6f..baadbe4 100644 --- a/readme.md +++ b/readme.md @@ -48,21 +48,31 @@ You can also activate features such as offline and silent mode: ```js const offline = true; const logger = true; +const snapshotAutoload = true; const snapshotLocation = './snapshot/'; const silentMode = true; const retryAfter = '5m'; let switcher = new Switcher(url, apiKey, domain, component, environment, { - offline, logger, snapshotLocation, silentMode, retryAfter + offline, logger, snapshotLocation, snapshotAutoload, silentMode, retryAfter }); ``` - **offline**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'. - **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Switcher.getLogger('KEY') - **snapshotLocation**: Location of snapshot files. The default value is './snapshot/'. +- **snapshotAutload**: If activated, snapshot folder and files are going to be created automatically. - **silentMode**: If activated, all connectivity issues will be ignored and the client will automatically fetch the configuration into your snapshot file. - **retryAfter** : Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours). +## Pre-execution +Before you call the API, there is one single step you need to execute to complete the configuration. +If you are not running the API expecting to use the offline features, you can ignore this step. + +After instantiating the Switcher, you need to load the snapshot engine to watch for changes in your Domain structure. +```js +await switcher.loadSnapshot(); +``` ## Executing There are a few different ways to call the API using the JavaScript module. @@ -77,10 +87,10 @@ await switcher.isItOn('FEATURE01'); ``` 2. **Promise** -Using promise is another way to call the API if you want, like: +Most functions were implemented using async operations. Here it is a differnet way to execute the criteria: ```js -switcher.isItOnPromise('KEY') +switcher.isItOn('KEY') .then(result => console.log('Result:', result)) .catch(error => console.log(error)); ``` @@ -119,4 +129,4 @@ For convenience, an implementation of a domain version checker is available if y ```js switcher.checkSnapshot(); -``` +``` \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts index e06e54a..4769067 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -23,7 +23,7 @@ declare class Switcher { /** * Validate the input provided to access the API */ - validate(): void; + validate(): Promise; /** * Pre-set input values before calling the API @@ -31,7 +31,7 @@ declare class Switcher { * @param key * @param input */ - prepare(key: string, input?: string[]): void; + prepare(key: string, input?: string[]): Promise; /** * Execute async criteria @@ -40,27 +40,18 @@ declare class Switcher { * @param input * @param showReason */ - isItOn(key?: string, input?: string[], showReason?: boolean): boolean; - - /** - * Execute async criteria - * - * @param key - * @param input - * @param showReason - */ - isItOnPromise(key?: string, input?: string[], showReason?: boolean): Promise; + isItOn(key?: string, input?: string[], showReason?: boolean): Promise; /** * Read snapshot file locally and store in a parsed JSON object */ - loadSnapshot(): void; + loadSnapshot(): Promise; /** * Verifies if the current snapshot file is updated. * Return true if an update has been made. */ - checkSnapshot(): boolean; + checkSnapshot(): Promise; /** * Remove snapshot from real-time update @@ -93,6 +84,7 @@ declare interface SwitcherOptions { offline: boolean; logger: boolean; snapshotLocation: string; + snapshotAutoload: string; silentMode: boolean; retryAfter: string; } diff --git a/src/index.js b/src/index.js index 31b6ddb..104ee29 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const DEFAULT_SNAPSHOT_LOCATION = './snapshot/'; const DEFAULT_RETRY_TIME = '5m'; const DEFAULT_OFFLINE = false; const DEFAULT_LOGGER = false; +const DEFAULT_SNAPSHOT_AUTOLOAD = false; class Switcher { @@ -36,6 +37,10 @@ class Switcher { this.snapshotLocation = options.snapshotLocation; } + if ('snapshotAutoload' in options) { + this.snapshotAutoload = options.snapshotAutoload; + } + if ('silentMode' in options) { this.silentMode = options.silentMode; } @@ -52,8 +57,6 @@ class Switcher { this.retryDurationIn = DEFAULT_RETRY_TIME.charAt(1); } } - - this.loadSnapshot(); } async prepare(key, input) { @@ -160,19 +163,19 @@ class Switcher { return false; } - isItOnPromise(key, input, showReason = false) { - return new Promise((resolve) => resolve(this.isItOn(key, input, showReason))); - } - - loadSnapshot() { + async loadSnapshot() { if (this.snapshotLocation) { const snapshotFile = `${this.snapshotLocation}${this.environment}.json`; - this.snapshot = loadDomain(snapshotFile); + this.snapshot = loadDomain(this.snapshotLocation, this.environment, this.snapshotAutoload); - fs.unwatchFile(snapshotFile); - fs.watchFile(snapshotFile, (curr, prev) => { - this.snapshot = loadDomain(snapshotFile); - }); + if (this.snapshot.data.domain.version == 0 && !this.offline) { + await this.checkSnapshot(); + } else { + fs.unwatchFile(snapshotFile); + fs.watchFile(snapshotFile, (curr, prev) => { + this.snapshot = loadDomain(this.snapshotLocation, this.environment, this.snapshotAutoload); + }); + } } } diff --git a/src/utils/index.js b/src/utils/index.js index f070603..e6983cc 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -2,9 +2,19 @@ const fs = require('fs'); const moment = require('moment'); const IPCIDR = require('ip-cidr'); -const loadDomain = (snapshotLocation) => { +const loadDomain = (snapshotLocation, environment, snapshotAutoload) => { try { - const dataBuffer = fs.readFileSync(snapshotLocation); + let dataBuffer; + const snapshotFile = `${snapshotLocation}${environment}.json`; + if (fs.existsSync(snapshotFile)) { + dataBuffer = fs.readFileSync(snapshotFile); + } else if (snapshotAutoload) { + dataBuffer = '{ "data": { "domain": { "version": 0 } } }'; + fs.mkdir(snapshotLocation, { recursive: true }, (err) => {}); + fs.writeFileSync(snapshotFile, dataBuffer); + } else { + throw new Error(); + } const dataJSON = dataBuffer.toString(); return JSON.parse(dataJSON); } catch (e) { diff --git a/test/playground/index.js b/test/playground/index.js index 6ebfe41..5f42010 100644 --- a/test/playground/index.js +++ b/test/playground/index.js @@ -12,6 +12,7 @@ function setupSwitcher(offline) { switcher = new Switcher(url, apiKey, domain, component, environment, { offline, logger: true }); + switcher.loadSnapshot(); } // Requires online API @@ -47,7 +48,7 @@ const testAsyncCall = async () => { let result = await switcher.isItOn('FEATURE2020'); console.log(result); - switcher.isItOnPromise('FEATURE2020') + switcher.isItOn('FEATURE2020') .then(result => console.log('Promise result:', result)) .catch(error => console.log(error)); @@ -75,4 +76,24 @@ const testBypasser = async () => { switcher.unloadSnapshot(); } -testSimpleAPICall(); \ No newline at end of file +// Requires online API +const testSnapshotAutoload = async () => { + const apiKey = '$2b$08$DYcg9NUcJouQkTtB6XxqeOQJ3DCprslij6Te.8aTF1Ds7y2sAvTjm' + const domain = 'My Domain' + const component = 'CustomerAPI' + const environment = 'generated' + const url = 'http://localhost:3000' + + switcher = new Switcher(url, apiKey, domain, component, environment, { + snapshotAutoload: true + }); + + await switcher.loadSnapshot(); + + let result = await switcher.isItOn('FEATURE2020'); + console.log(result); + + switcher.unloadSnapshot(); +} + +testSnapshotAutoload(); \ No newline at end of file diff --git a/test/test.js b/test/test.js index 77deac6..1ef06f3 100644 --- a/test/test.js +++ b/test/test.js @@ -18,6 +18,7 @@ describe('E2E test - Switcher offline:', function () { switcher = new Switcher(url, apiKey, domain, component, environment, { offline: true, logger: true }); + switcher.loadSnapshot(); }); this.afterAll(function() { @@ -26,7 +27,7 @@ describe('E2E test - Switcher offline:', function () { it('should be valid - isItOn', async function () { await switcher.prepare('FF2FOR2020', [Switcher.StrategiesType.VALUE, 'Japan', Switcher.StrategiesType.NETWORK, '10.0.0.3']); - await switcher.isItOn('FF2FOR2020').then(function (result) { + switcher.isItOn('FF2FOR2020').then(function (result) { assert.isTrue(result); assert.isNotEmpty(Switcher.getLogger('FF2FOR2020')); }, function (error) { @@ -34,14 +35,6 @@ describe('E2E test - Switcher offline:', function () { }); }); - it('should be valid - isItOnPromise', async function () { - await switcher.prepare('FF2FOR2020', [Switcher.StrategiesType.VALUE, 'Japan', Switcher.StrategiesType.NETWORK, '10.0.0.3']); - - switcher.isItOnPromise('FF2FOR2020') - .then(result => assert.isTrue(result)) - .catch(error => console.log('Rejected:', error)); - }); - it('should be valid - No prepare function called', async function () { switcher.isItOn('FF2FOR2020', [Switcher.StrategiesType.VALUE, 'Japan', Switcher.StrategiesType.NETWORK, '10.0.0.3']).then(function (result) { assert.isTrue(result); @@ -68,6 +61,7 @@ describe('E2E test - Switcher offline:', function () { }); it('should be valid assuming key to be false and then forgetting it', async function () { + await switcher.loadSnapshot(); await switcher.prepare('FF2FOR2020', [Switcher.StrategiesType.VALUE, 'Japan', Switcher.StrategiesType.NETWORK, '10.0.0.3']); assert.isTrue(await switcher.isItOn()); @@ -92,14 +86,35 @@ describe('E2E test - Switcher offline:', function () { }); }); - it('should be invalid - Offline file not found', async function () { + it('should be invalid - Offline mode did not found a snapshot file', async function () { try { - new Switcher(url, apiKey, domain, component, environment, { + const switcher = new Switcher(url, apiKey, domain, component, environment, { offline: true, snapshotLocation: 'somewhere/' }); + await switcher.loadSnapshot(); + assert.isNotNull(switcher.snapshot); + } catch (error) { + assert.equal('Something went wrong: It was not possible to load the file at somewhere/', error.message); + } + }); + + it('should be valid - Offline mode w/ autoload snapshot', async function () { + this.timeout(3000); + + try { + const switcher = new Switcher(url, apiKey, domain, component, environment, { + offline: true, + snapshotLocation: 'generated-snapshots/', + snapshotAutoload: true + }); + await switcher.loadSnapshot(); + assert.isNotNull(switcher.snapshot); + + switcher.unloadSnapshot(); + fs.unlinkSync(`generated-snapshots/${environment}.json`); } catch (error) { - assert.equal('Something went wrong: It was not possible to load the file at somewhere/default.json', error.message); + assert.equal('Something went wrong: It was not possible to load the file at generated-snapshots/', error.message); } }); }); @@ -272,6 +287,7 @@ describe('Unit test - Switcher:', function () { silentMode: true, retryAfter: '1s' }); + await switcher.loadSnapshot(); const spyPrepare = sinon.spy(switcher, 'prepare'); // First attempt to reach the online API - Since it's configured to use silent mode, it should return true (according to the snapshot) @@ -348,21 +364,23 @@ describe('E2E test - Switcher offline - Snapshot:', function () { let fsStub; afterEach(function() { - requestGetStub.restore(); - requestPostStub.restore(); - clientAuth.restore(); - fsStub.restore(); + if (requestGetStub != undefined) + requestGetStub.restore(); + + if (requestPostStub != undefined) + requestPostStub.restore(); + + if (clientAuth != undefined) + clientAuth.restore(); + + if (fsStub != undefined) + fsStub.restore(); }) beforeEach(function() { switcher = new Switcher(url, apiKey, domain, component, environment, { offline: true }); - - clientAuth = sinon.stub(services, 'auth'); - requestGetStub = sinon.stub(request, 'get'); - requestPostStub = sinon.stub(request, 'post'); - fsStub = sinon.stub(fs, 'writeFileSync'); }); this.afterAll(function() { @@ -371,25 +389,38 @@ describe('E2E test - Switcher offline - Snapshot:', function () { it('should update snapshot', async function () { // Mocking starts + clientAuth = sinon.stub(services, 'auth'); + requestGetStub = sinon.stub(request, 'get'); + requestPostStub = sinon.stub(request, 'post'); + clientAuth.returns(Promise.resolve({ token: 'uqwu1u8qj18j28wj28', exp: (Date.now()+5000)/1000 })); requestGetStub.returns(Promise.resolve({ status: false })); // Snapshot outdated requestPostStub.returns(Promise.resolve(JSON.stringify(JSON.parse(dataJSON), null, 4))); // Mocking finishes + await switcher.loadSnapshot(); assert.isTrue(await switcher.checkSnapshot()); }); it('should NOT update snapshot', async function () { // Mocking starts + clientAuth = sinon.stub(services, 'auth'); + requestGetStub = sinon.stub(request, 'get'); + clientAuth.returns(Promise.resolve({ token: 'uqwu1u8qj18j28wj28', exp: (Date.now()+5000)/1000 })); requestGetStub.returns(Promise.resolve({ status: true })); // No available update // Mocking finishes + await switcher.loadSnapshot(); assert.isFalse(await switcher.checkSnapshot()); }); it('should NOT update snapshot - check Snapshot Error', async function () { + this.timeout(3000); + // Mocking starts + clientAuth = sinon.stub(services, 'auth'); + clientAuth.returns(Promise.resolve({ token: 'uqwu1u8qj18j28wj28', exp: (Date.now()+5000)/1000 })); requestGetStub.throws({ error: { @@ -400,15 +431,20 @@ describe('E2E test - Switcher offline - Snapshot:', function () { }); // Mocking finishes + await switcher.loadSnapshot(); await switcher.checkSnapshot().then(function (result) { assert.isUndefined(result); }, function (error) { - assert.equal('Something went wrong: {"errno":"ECONNREFUSED","code":"ECONNREFUSED","syscall":"connect"}', error.message); + assert.equal('Something went wrong: {"errno":"ECONNREFUSED","code":"ECONNREFUSED","syscall":"connect","address":"127.0.0.1","port":3000}', error.message); }); }); it('should NOT update snapshot - resolve Snapshot Error', async function () { // Mocking starts + clientAuth = sinon.stub(services, 'auth'); + requestGetStub = sinon.stub(request, 'get'); + requestPostStub = sinon.stub(request, 'post'); + clientAuth.returns(Promise.resolve({ token: 'uqwu1u8qj18j28wj28', exp: (Date.now()+5000)/1000 })); requestGetStub.returns(Promise.resolve({ status: false })); // Snapshot outdated requestPostStub.throws({ @@ -420,6 +456,7 @@ describe('E2E test - Switcher offline - Snapshot:', function () { }); // Mocking finishes + await switcher.loadSnapshot(); await switcher.checkSnapshot().then(function (result) { assert.isUndefined(result); }, function (error) { @@ -427,4 +464,30 @@ describe('E2E test - Switcher offline - Snapshot:', function () { }); }); + it('should update snapshot - snapshot autoload activated', async function () { + // Mocking starts + clientAuth = sinon.stub(services, 'auth'); + requestGetStub = sinon.stub(request, 'get'); + requestPostStub = sinon.stub(request, 'post'); + + clientAuth.returns(Promise.resolve({ token: 'uqwu1u8qj18j28wj28', exp: (Date.now()+5000)/1000 })); + requestGetStub.returns(Promise.resolve({ status: false })); // Snapshot outdated + requestPostStub.returns(Promise.resolve(JSON.stringify(JSON.parse(dataJSON), null, 4))); + // Mocking finishes + + try { + const switcher = new Switcher(url, apiKey, domain, component, environment, { + snapshotLocation: 'generated-snapshots/', + snapshotAutoload: true + }); + await switcher.loadSnapshot(); + assert.isNotNull(switcher.snapshot); + + switcher.unloadSnapshot(); + fs.unlinkSync(`generated-snapshots/${environment}.json`); + } catch (error) { + assert.equal('Something went wrong: It was not possible to load the file at generated-snapshots/', error.message); + } + }); + }); \ No newline at end of file