@becollective/utils 1.6.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -9,7 +9,7 @@ import { getCurrencyFromCurrencyCode, makeMoneyString } from './src/money';
9
9
  import { readableOpportunityType } from './src/opportunity';
10
10
  import { getTimeInfo } from './src/opportunityUser';
11
11
  import { getHomeLocalityFromLocationList } from './src/locality';
12
- import { isFeatureActive } from './src/featureDecision';
12
+ import FeatureFlag from './src/FeatureFlag';
13
13
  const util = {
14
14
  getAge,
15
15
  getCurrencyFromCurrencyCode,
@@ -23,7 +23,7 @@ const util = {
23
23
  getTimeInfo,
24
24
  getHomeLocalityFromLocationList,
25
25
  getShiftText,
26
- isFeatureActive,
26
+ FeatureFlag,
27
27
  };
28
28
 
29
29
  module.exports = util;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becollective/utils",
3
- "version": "1.6.1",
3
+ "version": "1.7.2",
4
4
  "description": "Common utilities",
5
5
  "main": "bundle.js",
6
6
  "scripts": {
@@ -0,0 +1,50 @@
1
+ import axios from 'axios';
2
+ import { memoize, get as lodashGet } from 'lodash';
3
+
4
+ const fetchFeatureActive = async (
5
+ feature: string,
6
+ options: { env: string; region?: string },
7
+ ): Promise<boolean> => {
8
+ const getFlagByFeatureUrl = `https://lb-central.becollective.com/api/v2/feature-flags/${feature}`;
9
+ let isFeatureActive = false;
10
+ const featureFlags = await axios.get(getFlagByFeatureUrl);
11
+ const environments = lodashGet(featureFlags, 'data.Environments');
12
+ if (environments) {
13
+ isFeatureActive = environments.includes(options.env);
14
+ }
15
+ return isFeatureActive;
16
+ };
17
+
18
+ const memoizedFeatureActive = memoize(fetchFeatureActive, (feature, options) =>
19
+ JSON.stringify({ feature, options }),
20
+ );
21
+
22
+ class FeatureFlag {
23
+ ttl: number;
24
+ expiry: number;
25
+ constructor(ttl?) {
26
+ this.ttl = ttl || 1000 * 60 * 5; // default to 5 minutes
27
+ this.expiry = Date.now() + ttl;
28
+ }
29
+ async isFeatureActive(
30
+ feature: string,
31
+ options: { env: string; region?: string },
32
+ ): Promise<boolean> {
33
+ // Lazy expiration upon function call
34
+ const now = Date.now();
35
+ if (now >= this.expiry) {
36
+ memoizedFeatureActive.cache.clear();
37
+ this.expiry = now + this.ttl;
38
+ }
39
+ return await memoizedFeatureActive(feature, options);
40
+ }
41
+ clearCache() {
42
+ memoizedFeatureActive.cache.clear();
43
+ }
44
+ setCacheTtl(ttl: number) {
45
+ this.ttl = ttl;
46
+ this.expiry = Date.now() + ttl;
47
+ }
48
+ }
49
+
50
+ export default FeatureFlag;
package/src/password.js CHANGED
@@ -11,7 +11,7 @@ export const password = {
11
11
  validate: (input) => {
12
12
  if(typeof input !== 'string') {
13
13
  throw new Error('not-string');
14
- } else if(input.length < 8) {
14
+ } else if(input.length < 12) {
15
15
  throw new Error('invalid-length');
16
16
  }
17
17
 
@@ -0,0 +1,102 @@
1
+ import axios from 'axios';
2
+ import FeatureFlag from '../src/FeatureFlag';
3
+
4
+ const featureFlag = new FeatureFlag();
5
+ jest.mock('axios');
6
+ const mockAxios = axios as jest.Mocked<typeof axios>;
7
+ const feature = 'test-feature';
8
+
9
+ describe('Make feature decision based on feature flags', () => {
10
+ test('should return true if feature is flagged active on localtest', async () => {
11
+ mockAxios.get.mockReturnValue(
12
+ Promise.resolve({
13
+ data: { Environments: ['localtest', 'compose'] },
14
+ }),
15
+ );
16
+ const isActive = await featureFlag.isFeatureActive(feature, {
17
+ env: 'localtest',
18
+ });
19
+ expect(isActive).toBe(true);
20
+ });
21
+ test('should return false if feature is NOT flagged active on localtest', async () => {
22
+ featureFlag.clearCache();
23
+ mockAxios.get.mockReturnValue(
24
+ Promise.resolve({
25
+ data: { Environments: ['dev-au'] },
26
+ }),
27
+ );
28
+ const isActive = await featureFlag.isFeatureActive(feature, {
29
+ env: 'localtest',
30
+ });
31
+ expect(isActive).toBe(false);
32
+ });
33
+ test('should return false if no result returned from feature flag', async () => {
34
+ featureFlag.clearCache();
35
+ mockAxios.get.mockReturnValue(Promise.resolve({}));
36
+ const isActive = await featureFlag.isFeatureActive(feature, {
37
+ env: 'localtest',
38
+ });
39
+ expect(isActive).toBe(false);
40
+ });
41
+ describe('Test caching', () => {
42
+ test('should return same result for same parameter without fetch api', async () => {
43
+ featureFlag.clearCache();
44
+ mockAxios.get.mockReturnValue(
45
+ Promise.resolve({
46
+ data: { Environments: ['localtest', 'compose'] },
47
+ }),
48
+ );
49
+ const isActive = await featureFlag.isFeatureActive(feature, {
50
+ env: 'localtest',
51
+ });
52
+ expect(mockAxios.get).toBeCalled();
53
+ expect(isActive).toBe(true);
54
+ mockAxios.get.mockClear();
55
+ const secondCallResult = await featureFlag.isFeatureActive(feature, {
56
+ env: 'localtest',
57
+ });
58
+ expect(mockAxios.get).not.toBeCalled();
59
+ expect(secondCallResult).toBe(true);
60
+ });
61
+ test('should fetch api after expiry', async () => {
62
+ featureFlag.clearCache();
63
+ featureFlag.setCacheTtl(0);
64
+ mockAxios.get.mockReturnValue(
65
+ Promise.resolve({
66
+ data: { Environments: ['localtest', 'compose'] },
67
+ }),
68
+ );
69
+ const isActive = await featureFlag.isFeatureActive(feature, {
70
+ env: 'localtest',
71
+ });
72
+ expect(mockAxios.get).toBeCalled();
73
+ expect(isActive).toBe(true);
74
+ mockAxios.get.mockClear();
75
+ const secondCallResult = await featureFlag.isFeatureActive(feature, {
76
+ env: 'localtest',
77
+ });
78
+ expect(mockAxios.get).toBeCalled();
79
+ expect(secondCallResult).toBe(true);
80
+ });
81
+ test('should fetch api for different parameter', async () => {
82
+ featureFlag.clearCache();
83
+ featureFlag.setCacheTtl(1000 * 60 * 5);
84
+ mockAxios.get.mockReturnValue(
85
+ Promise.resolve({
86
+ data: { Environments: ['localtest', 'compose'] },
87
+ }),
88
+ );
89
+ const isActive = await featureFlag.isFeatureActive(feature, {
90
+ env: 'localtest',
91
+ });
92
+ expect(mockAxios.get).toBeCalled();
93
+ expect(isActive).toBe(true);
94
+ mockAxios.get.mockClear();
95
+ const secondCallResult = await featureFlag.isFeatureActive(feature, {
96
+ env: 'compose',
97
+ });
98
+ expect(mockAxios.get).toBeCalled();
99
+ expect(secondCallResult).toBe(true);
100
+ });
101
+ });
102
+ });
@@ -2,7 +2,7 @@ const util = require('../bundle.js');
2
2
 
3
3
  describe('password.valiate', () => {
4
4
  test('validate a valid password', async () => {
5
- const password = 'Password123';
5
+ const password = 'Password123456';
6
6
  let result;
7
7
  try {
8
8
  result = util.password.validate(password);
@@ -18,13 +18,13 @@ describe('password.valiate', () => {
18
18
  }).toThrow('invalid-length');
19
19
  }, 1000);
20
20
  test('validate a one rule password', async () => {
21
- const password = 'foobarab';
21
+ const password = 'foobarabpcip';
22
22
  expect(() => {
23
23
  util.password.validate(password);
24
24
  }).toThrow('invalid-minimum-rules');
25
25
  }, 1000);
26
26
  test('validate a two rule password', async () => {
27
- const password = 'foobar123';
27
+ const password = 'foobarpcip123';
28
28
  expect(() => {
29
29
  util.password.validate(password);
30
30
  }).toThrow('invalid-minimum-rules');
@@ -1,14 +0,0 @@
1
- import axios from 'axios';
2
-
3
- export const isFeatureActive = async (
4
- feature: string,
5
- options: { env: string; region?: string },
6
- ): Promise<boolean> => {
7
- const getFlagByFeatureUrl = `https://lb-central.becollective.com/api/v2/feature-flags/${feature}`;
8
- let isFeatureActive = false;
9
- const featureFlags = await axios.get(getFlagByFeatureUrl);
10
- if (featureFlags && featureFlags.data && featureFlags.data.Environments) {
11
- isFeatureActive = featureFlags.data.Environments.includes(options.env);
12
- }
13
- return isFeatureActive;
14
- };
@@ -1,38 +0,0 @@
1
- import { isFeatureActive } from '../src/featureDecision';
2
- import axios from 'axios';
3
-
4
- jest.mock('axios');
5
- const mockAxios = axios as jest.Mocked<typeof axios>;
6
- const feature = 'test-feature';
7
-
8
- describe('Make feature decision based on feature flags', () => {
9
- test('should return true if feature is flagged active on localtest', async () => {
10
- mockAxios.get.mockReturnValue(
11
- Promise.resolve({
12
- data: { Environments: ['localtest', 'compose'] },
13
- }),
14
- );
15
- const isActive = await isFeatureActive(feature, {
16
- env: 'localtest',
17
- });
18
- expect(isActive).toBe(true);
19
- });
20
- test('should return false if feature is NOT flagged active on localtest', async () => {
21
- mockAxios.get.mockReturnValue(
22
- Promise.resolve({
23
- data: { Environments: ['dev-au'] },
24
- }),
25
- );
26
- const isActive = await isFeatureActive(feature, {
27
- env: 'localtest',
28
- });
29
- expect(isActive).toBe(false);
30
- });
31
- test('should return false if no result returned from feature flag', async () => {
32
- mockAxios.get.mockReturnValue(Promise.resolve({}));
33
- const isActive = await isFeatureActive(feature, {
34
- env: 'localtest',
35
- });
36
- expect(isActive).toBe(false);
37
- });
38
- });