From 7f5f345b13d0139ca9de5c454858a81aa7f60b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 20 Jan 2022 21:58:09 +0100 Subject: [PATCH 01/12] feat: add support for new constraint operators --- examples/simple_usage.js | 12 ++++---- src/helpers.ts | 6 ++-- src/strategy/strategy.ts | 39 ++++++++++++++++++++++--- test/strategy/strategy-test.js | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/examples/simple_usage.js b/examples/simple_usage.js index f22458b5..26fb0cf7 100644 --- a/examples/simple_usage.js +++ b/examples/simple_usage.js @@ -1,8 +1,9 @@ -const { initialize, isEnabled } = require('../lib'); +const { initialize } = require('../lib'); const client = initialize({ appName: 'my-application', - url: 'http://unleash.herokuapp.com/api/', + url: 'http://localhost:3000/api/', + refreshInterval: 1000, customHeaders: { Authorization: '*:development.ba76487db29d7ef2557977a25b477c2e6288e2d9334fd1b91f63e2a9', } @@ -14,8 +15,9 @@ client.on('ready', () => { console.log('ready!') }); -console.log('Fetching toggles from: http://unleash.herokuapp.com'); - setInterval(() => { - console.log(`featureX enabled: ${isEnabled('featureX')}`); + const context = { properties: {email: 'ivar@getunleash.ai'}}; + const toggleStatus = client.isEnabled('TestOperator', context); + + console.log(`TestOperator: ${toggleStatus ? 'on' : 'off'}`); }, 1000); diff --git a/src/helpers.ts b/src/helpers.ts index 8dd1d907..fa84d827 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -17,12 +17,12 @@ export function createFallbackFunction( return () => false; } -export function resolveContextValue(context: Context, field: string) { +export function resolveContextValue(context: Context, field: string): string | undefined { if (context[field]) { - return context[field]; + return context[field] as string; } if (context.properties && context.properties[field]) { - return context.properties[field]; + return context.properties[field] as string; } return undefined; } diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index bce9fdbe..676dc667 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -14,8 +14,11 @@ export interface Constraint { } export enum Operator { - IN = 'IN', - NOT_IN = 'NOT_IN', + IN = 'IN', + NOT_IN = 'NOT_IN', + ENDS_WITH = 'ENDS_WITH', + STARTS_WITH = 'STARTS_WITH', + CONTAINS = 'CONTAINS', } export class Strategy { @@ -28,11 +31,39 @@ export class Strategy { this.returnValue = returnValue; } + + checkConstraint(constraint: Constraint, context: Context) { const field = constraint.contextName; const contextValue = resolveContextValue(context, field); - const isIn = constraint.values.some((val) => val.trim() === contextValue); - return constraint.operator === Operator.IN ? isIn : !isIn; + switch (constraint.operator) { + case Operator.IN: + case Operator.NOT_IN: { + const isIn = constraint.values.some((val) => val.trim() === contextValue); + return constraint.operator === Operator.IN ? isIn : !isIn; + } + case Operator.ENDS_WITH: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.endsWith(val.trim())); + } + case Operator.STARTS_WITH: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.startsWith(val.trim())); + } + case Operator.CONTAINS: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.includes(val.trim())); + } + default: + return false; + } + } checkConstraints(context: Context, constraints?: Constraint[]) { diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index dc312151..e1b48433 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -105,3 +105,56 @@ test('should be enabled when cosutomerId is in constraint', (t) => { }; t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); + + +test('should be enabled when email startsWith', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STARTS_WITH', values: ['example'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when email endsWith', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'ENDS_WITH', values: ['@getunleash.ai'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should not enabled when email does not endsWith', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'ENDS_WITH', values: ['@getunleash.ai'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@something-else.ai' }, + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when email contains', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'CONTAINS', values: ['some'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example-some@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); \ No newline at end of file From 950ef9164955b219daa420dc79a079145da1d79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 20 Jan 2022 21:59:17 +0100 Subject: [PATCH 02/12] fix: revert simple usage --- examples/simple_usage.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/simple_usage.js b/examples/simple_usage.js index 26fb0cf7..87c9f4cc 100644 --- a/examples/simple_usage.js +++ b/examples/simple_usage.js @@ -1,4 +1,4 @@ -const { initialize } = require('../lib'); +const { initialize, isEnabled } = require('../lib'); const client = initialize({ appName: 'my-application', @@ -15,9 +15,8 @@ client.on('ready', () => { console.log('ready!') }); -setInterval(() => { - const context = { properties: {email: 'ivar@getunleash.ai'}}; - const toggleStatus = client.isEnabled('TestOperator', context); +console.log('Fetching toggles from: http://unleash.herokuapp.com'); - console.log(`TestOperator: ${toggleStatus ? 'on' : 'off'}`); -}, 1000); +setInterval(() => { + console.log(`featureX enabled: ${isEnabled('featureX')}`); +}, 1000); \ No newline at end of file From cd50ed53a90e2b41c1dc65045539be28281ce516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 20 Jan 2022 22:01:59 +0100 Subject: [PATCH 03/12] fix: simplify swtich/case --- src/strategy/strategy.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index 676dc667..25c7e682 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -36,6 +36,9 @@ export class Strategy { checkConstraint(constraint: Constraint, context: Context) { const field = constraint.contextName; const contextValue = resolveContextValue(context, field); + if(!contextValue) { + return false; + } switch (constraint.operator) { case Operator.IN: case Operator.NOT_IN: { @@ -43,21 +46,12 @@ export class Strategy { return constraint.operator === Operator.IN ? isIn : !isIn; } case Operator.ENDS_WITH: { - if(!contextValue) { - return false; - } return constraint.values.some((val) => contextValue.endsWith(val.trim())); } case Operator.STARTS_WITH: { - if(!contextValue) { - return false; - } return constraint.values.some((val) => contextValue.startsWith(val.trim())); } case Operator.CONTAINS: { - if(!contextValue) { - return false; - } return constraint.values.some((val) => contextValue.includes(val.trim())); } default: From b0d1051aa38f5b7cf3174a764ee089120756bdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 21 Jan 2022 10:30:11 +0100 Subject: [PATCH 04/12] fix: more operators --- src/strategy/strategy.ts | 23 +++++++++--- test/strategy/strategy-test.js | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index 25c7e682..c30f4c86 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -19,6 +19,9 @@ export enum Operator { ENDS_WITH = 'ENDS_WITH', STARTS_WITH = 'STARTS_WITH', CONTAINS = 'CONTAINS', + NUM_GT = 'NUM_GT', + NUM_LT = 'NUM_LT', + NUM_EQ = 'NUM_EQ', } export class Strategy { @@ -36,23 +39,35 @@ export class Strategy { checkConstraint(constraint: Constraint, context: Context) { const field = constraint.contextName; const contextValue = resolveContextValue(context, field); + const values = constraint.values + .filter(v => !!v) + .map(v => v.trim()); if(!contextValue) { return false; } switch (constraint.operator) { case Operator.IN: case Operator.NOT_IN: { - const isIn = constraint.values.some((val) => val.trim() === contextValue); + const isIn = values.some(val => val === contextValue); return constraint.operator === Operator.IN ? isIn : !isIn; } case Operator.ENDS_WITH: { - return constraint.values.some((val) => contextValue.endsWith(val.trim())); + return values.some(val => contextValue.endsWith(val)); } case Operator.STARTS_WITH: { - return constraint.values.some((val) => contextValue.startsWith(val.trim())); + return values.some(val => contextValue.startsWith(val)); } case Operator.CONTAINS: { - return constraint.values.some((val) => contextValue.includes(val.trim())); + return values.some(val => contextValue.includes(val)); + } + case Operator.NUM_EQ: { + return Number(values[0]) === Number(contextValue); + } + case Operator.NUM_GT: { + return Number(values[0]) < Number(contextValue); + } + case Operator.NUM_LT: { + return Number(values[0]) > Number(contextValue); } default: return false; diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index e1b48433..16f60508 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -157,4 +157,69 @@ test('should be enabled when email contains', (t) => { properties: { email: 'example-some@getunleash.ai' }, }; t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "equals"', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_EQ', values: ['42'] }, + ]; + const context = { + environment: 'dev', + properties: { someVal: '42' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "equals" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_EQ', values: ['42'] }, + ]; + const context = { + environment: 'dev', + properties: { someVal: 42 }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "greater than" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_GT', values: ['42'] }, + ]; + const context = { + environment: 'dev', + properties: { someVal: '44' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be disable when someVal is not "greater than" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_GT', values: ['42'] }, + ]; + const context = { + environment: 'dev', + properties: { someVal: '42' }, + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "lower than" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_LT', values: ['42'] }, + ]; + const context = { + environment: 'dev', + properties: { someVal: '0' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); \ No newline at end of file From 5532915f8ad9ce131131d92891fdb59af99a2ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 21 Jan 2022 10:43:00 +0100 Subject: [PATCH 05/12] fix: add example for context fields --- examples/constraint-operators.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/constraint-operators.js diff --git a/examples/constraint-operators.js b/examples/constraint-operators.js new file mode 100644 index 00000000..931b2a8e --- /dev/null +++ b/examples/constraint-operators.js @@ -0,0 +1,26 @@ +const { initialize, isEnabled } = require('../lib'); + +const client = initialize({ + appName: 'my-application', + url: 'http://localhost:4242/api/', + refreshInterval: 1000, + customHeaders: { + Authorization: '*:development.7d9ca8d289c1545f1e28a6e4b2e25453c6cfc90346876ac7240c6668' }, +}); + +client.on('error', console.error); +client.on('warn', console.log); + +console.log('Fetching toggles from: http://unleash.herokuapp.com'); + +setInterval(() => { + const toggle = 'TestOperator'; + const context = { + properties: { + email: 'ivar@getunleash.ai', + age: 37 + } + } + const enabled = isEnabled(toggle, context); + console.log(`${toggle}: ${enabled ? 'on' : 'off'}`); +}, 1000); \ No newline at end of file From b7abafde6961d66c0cff71a82f96057737d81532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 20 Jan 2022 21:58:09 +0100 Subject: [PATCH 06/12] feat: add support for new constraint operators --- examples/simple_usage.js | 11 ++++++----- src/strategy/strategy.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/examples/simple_usage.js b/examples/simple_usage.js index 87c9f4cc..26fb0cf7 100644 --- a/examples/simple_usage.js +++ b/examples/simple_usage.js @@ -1,4 +1,4 @@ -const { initialize, isEnabled } = require('../lib'); +const { initialize } = require('../lib'); const client = initialize({ appName: 'my-application', @@ -15,8 +15,9 @@ client.on('ready', () => { console.log('ready!') }); -console.log('Fetching toggles from: http://unleash.herokuapp.com'); - setInterval(() => { - console.log(`featureX enabled: ${isEnabled('featureX')}`); -}, 1000); \ No newline at end of file + const context = { properties: {email: 'ivar@getunleash.ai'}}; + const toggleStatus = client.isEnabled('TestOperator', context); + + console.log(`TestOperator: ${toggleStatus ? 'on' : 'off'}`); +}, 1000); diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index c30f4c86..96a4e41a 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -19,9 +19,12 @@ export enum Operator { ENDS_WITH = 'ENDS_WITH', STARTS_WITH = 'STARTS_WITH', CONTAINS = 'CONTAINS', +<<<<<<< HEAD NUM_GT = 'NUM_GT', NUM_LT = 'NUM_LT', NUM_EQ = 'NUM_EQ', +======= +>>>>>>> b310e06 (feat: add support for new constraint operators) } export class Strategy { @@ -39,6 +42,7 @@ export class Strategy { checkConstraint(constraint: Constraint, context: Context) { const field = constraint.contextName; const contextValue = resolveContextValue(context, field); +<<<<<<< HEAD const values = constraint.values .filter(v => !!v) .map(v => v.trim()); @@ -68,6 +72,31 @@ export class Strategy { } case Operator.NUM_LT: { return Number(values[0]) > Number(contextValue); +======= + switch (constraint.operator) { + case Operator.IN: + case Operator.NOT_IN: { + const isIn = constraint.values.some((val) => val.trim() === contextValue); + return constraint.operator === Operator.IN ? isIn : !isIn; + } + case Operator.ENDS_WITH: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.endsWith(val.trim())); + } + case Operator.STARTS_WITH: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.startsWith(val.trim())); + } + case Operator.CONTAINS: { + if(!contextValue) { + return false; + } + return constraint.values.some((val) => contextValue.includes(val.trim())); +>>>>>>> b310e06 (feat: add support for new constraint operators) } default: return false; From efc0c433f00eb5f1611be045d1d4d47a0425844c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 20 Jan 2022 21:59:17 +0100 Subject: [PATCH 07/12] fix: revert simple usage --- examples/simple_usage.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/simple_usage.js b/examples/simple_usage.js index 26fb0cf7..87c9f4cc 100644 --- a/examples/simple_usage.js +++ b/examples/simple_usage.js @@ -1,4 +1,4 @@ -const { initialize } = require('../lib'); +const { initialize, isEnabled } = require('../lib'); const client = initialize({ appName: 'my-application', @@ -15,9 +15,8 @@ client.on('ready', () => { console.log('ready!') }); -setInterval(() => { - const context = { properties: {email: 'ivar@getunleash.ai'}}; - const toggleStatus = client.isEnabled('TestOperator', context); +console.log('Fetching toggles from: http://unleash.herokuapp.com'); - console.log(`TestOperator: ${toggleStatus ? 'on' : 'off'}`); -}, 1000); +setInterval(() => { + console.log(`featureX enabled: ${isEnabled('featureX')}`); +}, 1000); \ No newline at end of file From d57cf5d8c0b5bac3b7ea39126c23c022e1d19e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Sat, 29 Jan 2022 15:17:13 +0100 Subject: [PATCH 08/12] fix: refactor new constraint operators --- src/strategy/strategy.ts | 156 +++++++++++++++++++-------------- test/strategy/strategy-test.js | 111 +++++++++++++++++++++-- 2 files changed, 192 insertions(+), 75 deletions(-) diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index 96a4e41a..2542860e 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -10,23 +10,97 @@ export interface StrategyTransportInterface { export interface Constraint { contextName: string; operator: Operator; + inverted: boolean; values: string[]; + value?: string | number | Date; } export enum Operator { IN = 'IN', NOT_IN = 'NOT_IN', - ENDS_WITH = 'ENDS_WITH', - STARTS_WITH = 'STARTS_WITH', - CONTAINS = 'CONTAINS', -<<<<<<< HEAD + STR_ENDS_WITH = 'STR_ENDS_WITH', + STR_STARTS_WITH = 'STR_STARTS_WITH', + STR_CONTAINS = 'STR_CONTAINS', + NUM_EQ = 'NUM_EQ', NUM_GT = 'NUM_GT', + NUM_GTE = 'NUM_GTE', NUM_LT = 'NUM_LT', - NUM_EQ = 'NUM_EQ', -======= ->>>>>>> b310e06 (feat: add support for new constraint operators) + NUM_LTE = 'NUM_LTE', +} + +export type OperatorImpl = (constraint: Constraint, context: Context) => boolean; + +const cleanValues = (values: string[]) => values + .filter(v => !!v) + .map(v => v.trim()) + +const InOperator = (constraint: Constraint, context: Context) => { + const field = constraint.contextName; + const values = cleanValues(constraint.values); + const contextValue = resolveContextValue(context, field); + if(!contextValue) { + return false; + } + + const isIn = values.some(val => val === contextValue); + return constraint.operator === Operator.IN ? isIn : !isIn; +} + +const StringOperator = (constraint: Constraint, context: Context) => { + const field = constraint.contextName; + const {operator} = constraint; + const values = cleanValues(constraint.values); + const contextValue = resolveContextValue(context, field); + if(!contextValue) { + return false; + } + + if(operator === Operator.STR_STARTS_WITH) { + return values.some(val => contextValue.startsWith(val)); + } if(operator === Operator.STR_ENDS_WITH) { + return values.some(val => contextValue.endsWith(val)); + } if(operator === Operator.STR_CONTAINS) { + return values.some(val => contextValue.includes(val)); + } + return false; } +const NumberOperator = (constraint: Constraint, context: Context) => { + const field = constraint.contextName; + const {operator} = constraint; + const value = Number(constraint.value); + const contextValue = Number(resolveContextValue(context, field)); + + if(Number.isNaN(value) || Number.isNaN(contextValue)) { + return false; + } + + if(operator === Operator.NUM_EQ) { + return contextValue === value; + } if(operator === Operator.NUM_GT) { + return contextValue > value; + } if(operator === Operator.NUM_GTE) { + return contextValue >= value; + } if(operator === Operator.NUM_LT) { + return contextValue < value; + } if(operator === Operator.NUM_LTE) { + return contextValue <= value; + } + return false; +} + +const operators = new Map(); +operators.set(Operator.IN, InOperator); +operators.set(Operator.NOT_IN, InOperator); +operators.set(Operator.STR_STARTS_WITH, StringOperator); +operators.set(Operator.STR_ENDS_WITH, StringOperator); +operators.set(Operator.STR_CONTAINS, StringOperator); +operators.set(Operator.NUM_EQ, NumberOperator); +operators.set(Operator.NUM_LT, NumberOperator); +operators.set(Operator.NUM_LTE, NumberOperator); +operators.set(Operator.NUM_GT, NumberOperator); +operators.set(Operator.NUM_GTE, NumberOperator); + export class Strategy { public name: string; @@ -40,67 +114,17 @@ export class Strategy { checkConstraint(constraint: Constraint, context: Context) { - const field = constraint.contextName; - const contextValue = resolveContextValue(context, field); -<<<<<<< HEAD - const values = constraint.values - .filter(v => !!v) - .map(v => v.trim()); - if(!contextValue) { + const evaluator = operators.get(constraint.operator); + + if(!evaluator) { return false; } - switch (constraint.operator) { - case Operator.IN: - case Operator.NOT_IN: { - const isIn = values.some(val => val === contextValue); - return constraint.operator === Operator.IN ? isIn : !isIn; - } - case Operator.ENDS_WITH: { - return values.some(val => contextValue.endsWith(val)); - } - case Operator.STARTS_WITH: { - return values.some(val => contextValue.startsWith(val)); - } - case Operator.CONTAINS: { - return values.some(val => contextValue.includes(val)); - } - case Operator.NUM_EQ: { - return Number(values[0]) === Number(contextValue); - } - case Operator.NUM_GT: { - return Number(values[0]) < Number(contextValue); - } - case Operator.NUM_LT: { - return Number(values[0]) > Number(contextValue); -======= - switch (constraint.operator) { - case Operator.IN: - case Operator.NOT_IN: { - const isIn = constraint.values.some((val) => val.trim() === contextValue); - return constraint.operator === Operator.IN ? isIn : !isIn; - } - case Operator.ENDS_WITH: { - if(!contextValue) { - return false; - } - return constraint.values.some((val) => contextValue.endsWith(val.trim())); - } - case Operator.STARTS_WITH: { - if(!contextValue) { - return false; - } - return constraint.values.some((val) => contextValue.startsWith(val.trim())); - } - case Operator.CONTAINS: { - if(!contextValue) { - return false; - } - return constraint.values.some((val) => contextValue.includes(val.trim())); ->>>>>>> b310e06 (feat: add support for new constraint operators) - } - default: - return false; - } + + if(constraint.inverted) { + return !evaluator(constraint, context); + } + + return evaluator(constraint, context); } diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index 16f60508..6299d8d1 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -106,12 +106,26 @@ test('should be enabled when cosutomerId is in constraint', (t) => { t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); +// New constraint operators test('should be enabled when email startsWith', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'email', operator: 'STARTS_WITH', values: ['example'] }, + { contextName: 'email', operator: 'STR_STARTS_WITH', values: ['example'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when email startsWith (multiple)', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STR_STARTS_WITH', values: ['other', 'example'] }, ]; const context = { environment: 'dev', @@ -124,7 +138,34 @@ test('should be enabled when email endsWith', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'email', operator: 'ENDS_WITH', values: ['@getunleash.ai'] }, + { contextName: 'email', operator: 'STR_ENDS_WITH', values: ['@getunleash.ai'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when email NOT endsWith (inverted)', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STR_ENDS_WITH', values: ['@getunleash.ai'], inverted: true }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@something-else.com' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when email endsWith (multi)', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STR_ENDS_WITH', + values: ['@getunleash.ai', '@somerandom-email.com'] }, ]; const context = { environment: 'dev', @@ -137,7 +178,7 @@ test('should not enabled when email does not endsWith', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'email', operator: 'ENDS_WITH', values: ['@getunleash.ai'] }, + { contextName: 'email', operator: 'STR_ENDS_WITH', values: ['@getunleash.ai'] }, ]; const context = { environment: 'dev', @@ -150,7 +191,7 @@ test('should be enabled when email contains', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'email', operator: 'CONTAINS', values: ['some'] }, + { contextName: 'email', operator: 'STR_CONTAINS', values: ['some'] }, ]; const context = { environment: 'dev', @@ -159,11 +200,24 @@ test('should be enabled when email contains', (t) => { t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); +test('should be enabled when email does not contain (inverted)', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STR_CONTAINS', values: ['some'], inverted: true }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@getunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + test('should be enabled when someVal "equals"', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'NUM_EQ', values: ['42'] }, + { contextName: 'someVal', operator: 'NUM_EQ', value: 42 }, ]; const context = { environment: 'dev', @@ -172,11 +226,24 @@ test('should be enabled when someVal "equals"', (t) => { t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); +test('should be enabled when someVal not "equals" (inverted)', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_EQ', value: 42, inverted: true }, + ]; + const context = { + environment: 'dev', + properties: { someVal: '44' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + test('should be enabled when someVal "equals" number', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'NUM_EQ', values: ['42'] }, + { contextName: 'someVal', operator: 'NUM_EQ', value: 42 }, ]; const context = { environment: 'dev', @@ -189,7 +256,7 @@ test('should be enabled when someVal "greater than" number', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'NUM_GT', values: ['42'] }, + { contextName: 'someVal', operator: 'NUM_GT', value: 42 }, ]; const context = { environment: 'dev', @@ -202,7 +269,7 @@ test('should be disable when someVal is not "greater than" number', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'NUM_GT', values: ['42'] }, + { contextName: 'someVal', operator: 'NUM_GT', value: 42 }, ]; const context = { environment: 'dev', @@ -215,11 +282,37 @@ test('should be enabled when someVal "lower than" number', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'NUM_LT', values: ['42'] }, + { contextName: 'someVal', operator: 'NUM_LT', value: 42 }, ]; const context = { environment: 'dev', properties: { someVal: '0' }, }; t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "lower than or eq" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_LTE', value: '42' }, + ]; + const context = { + environment: 'dev', + properties: { someVal: 42 }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when someVal "greater than or eq" number', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'NUM_GTE', value: '42' }, + ]; + const context = { + environment: 'dev', + properties: { someVal: 42 }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); \ No newline at end of file From 8b99d72ca50e038ea924828524ef8f02f1797ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Sat, 29 Jan 2022 15:52:46 +0100 Subject: [PATCH 09/12] fix: add support for date operators --- src/context.ts | 3 +- src/strategy/strategy.ts | 40 ++++++++++---- test/strategy/strategy-test.js | 96 ++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/context.ts b/src/context.ts index ae9a2590..530266d7 100644 --- a/src/context.ts +++ b/src/context.ts @@ -3,7 +3,8 @@ export interface Properties { } export interface Context { - [key: string]: string | undefined | number | Properties; + [key: string]: string | Date | undefined | number | Properties; + currentTime?: Date; userId?: string; sessionId?: string; remoteAddress?: string; diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index 2542860e..4a9a7222 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -13,6 +13,7 @@ export interface Constraint { inverted: boolean; values: string[]; value?: string | number | Date; + caseInsensitive?: boolean; } export enum Operator { @@ -26,6 +27,8 @@ export enum Operator { NUM_GTE = 'NUM_GTE', NUM_LT = 'NUM_LT', NUM_LTE = 'NUM_LTE', + DATE_AFTER = 'DATE_AFTER', + DATE_BEFORE = 'DATE_BEFORE', } export type OperatorImpl = (constraint: Constraint, context: Context) => boolean; @@ -47,24 +50,38 @@ const InOperator = (constraint: Constraint, context: Context) => { } const StringOperator = (constraint: Constraint, context: Context) => { - const field = constraint.contextName; - const {operator} = constraint; - const values = cleanValues(constraint.values); - const contextValue = resolveContextValue(context, field); - if(!contextValue) { - return false; + const { contextName, operator, caseInsensitive} = constraint; + let values = cleanValues(constraint.values); + let contextValue = resolveContextValue(context, contextName); + + if(caseInsensitive) { + values = values.map(v => v.toLocaleLowerCase()); + contextValue = contextValue?.toLocaleLowerCase(); } if(operator === Operator.STR_STARTS_WITH) { - return values.some(val => contextValue.startsWith(val)); + return values.some(val => contextValue?.startsWith(val)); } if(operator === Operator.STR_ENDS_WITH) { - return values.some(val => contextValue.endsWith(val)); + return values.some(val => contextValue?.endsWith(val)); } if(operator === Operator.STR_CONTAINS) { - return values.some(val => contextValue.includes(val)); + return values.some(val => contextValue?.includes(val)); } return false; } +const DateOperator = (constraint: Constraint, context: Context) => { + const { operator } = constraint; + const value = constraint.value as Date; + const currentTime = context.currentTime || new Date(); + + if(operator === Operator.DATE_AFTER) { + return currentTime > value; + } if(operator === Operator.DATE_BEFORE) { + return currentTime < value; + } + return false; +} + const NumberOperator = (constraint: Constraint, context: Context) => { const field = constraint.contextName; const {operator} = constraint; @@ -100,7 +117,8 @@ operators.set(Operator.NUM_LT, NumberOperator); operators.set(Operator.NUM_LTE, NumberOperator); operators.set(Operator.NUM_GT, NumberOperator); operators.set(Operator.NUM_GTE, NumberOperator); - +operators.set(Operator.DATE_AFTER, DateOperator); +operators.set(Operator.DATE_BEFORE, DateOperator); export class Strategy { public name: string; @@ -115,7 +133,7 @@ export class Strategy { checkConstraint(constraint: Constraint, context: Context) { const evaluator = operators.get(constraint.operator); - + if(!evaluator) { return false; } diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index 6299d8d1..67b85d1f 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -147,6 +147,35 @@ test('should be enabled when email endsWith', (t) => { t.true(strategy.isEnabledWithConstraints(params, context, constraints)); }); +test('should be enabled when email endsWith, ignoring case', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [{ + contextName: 'email', + operator: 'STR_ENDS_WITH', + values: ['@getunleash.ai'], + caseInsensitive: true, + }]; + const context = { + environment: 'dev', + properties: { email: 'example@GETunleash.ai' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should not be enabled when email endsWith, caring about case', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'email', operator: 'STR_ENDS_WITH', values: ['@getunleash.ai'] }, + ]; + const context = { + environment: 'dev', + properties: { email: 'example@GETunleash.ai' }, + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + test('should be enabled when email NOT endsWith (inverted)', (t) => { const strategy = new Strategy('test', true); const params = {}; @@ -315,4 +344,71 @@ test('should be enabled when someVal "greater than or eq" number', (t) => { properties: { someVal: 42 }, }; t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + + +test('should be enabled when date is after', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'DATE_AFTER', value: new Date("2022-01-29T13:00:00.000Z") }, + ]; + const context = { + environment: 'dev', + currentTime: new Date("2022-01-29T14:00:00.000Z"), + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be disabled when date is not after', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'DATE_AFTER', value: new Date("2022-01-29T13:00:00.000Z") }, + ]; + const context = { + environment: 'dev', + currentTime: new Date("2022-01-27T14:00:00.000Z"), + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when date is after implicit currentTime', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', operator: 'DATE_AFTER', value: new Date("2022-01-28T14:00:00.000Z")}, + ]; + const context = { + environment: 'dev', + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when date is before', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', + operator: 'DATE_BEFORE', value: new Date("2022-01-29T13:00:00.000Z") }, + ]; + const context = { + environment: 'dev', + currentTime: new Date("2022-01-27T14:00:00.000Z"), + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be disabled when date is not before', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'someVal', + operator: 'DATE_BEFORE', value: new Date("2022-01-25T13:00:00.000Z") }, + ]; + const context = { + environment: 'dev', + currentTime: new Date("2022-01-27T14:00:00.000Z"), + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); }); \ No newline at end of file From 056787602c19591481e1ddc88db999769e419629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Sat, 29 Jan 2022 16:06:52 +0100 Subject: [PATCH 10/12] fix: add support for semver ranges --- package.json | 4 +- src/strategy/strategy.ts | 25 ++++++++++ test/strategy/strategy-test.js | 88 ++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b73a9a3f..53529472 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "ip": "^1.1.5", "make-fetch-happen": "^9.0.4", "murmurhash3js": "^3.0.1", - "nodemon": "^2.0.15" + "nodemon": "^2.0.15", + "semver": "^7.3.5" }, "engines": { "node": ">=10", @@ -47,6 +48,7 @@ "@types/make-fetch-happen": "9.0.1", "@types/murmurhash3js": "3.0.2", "@types/node": "14.18.9", + "@types/semver": "^7.3.9", "@typescript-eslint/eslint-plugin": "5.10.0", "@unleash/client-specification": "4.0.0", "ava": "3.15.0", diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index 4a9a7222..f58287bf 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -1,3 +1,4 @@ +import { gt as semverGt, lt as semverLt, eq as semverEq } from 'semver'; import { Context } from '../context'; import { resolveContextValue } from '../helpers'; @@ -29,6 +30,9 @@ export enum Operator { NUM_LTE = 'NUM_LTE', DATE_AFTER = 'DATE_AFTER', DATE_BEFORE = 'DATE_BEFORE', + SEMVER_EQ = 'SEMVER_EQ', + SEMVER_GT = 'SEMVER_GT', + SEMVER_LT = 'SEMVER_LT', } export type OperatorImpl = (constraint: Constraint, context: Context) => boolean; @@ -69,6 +73,24 @@ const StringOperator = (constraint: Constraint, context: Context) => { return false; } +const SemverOperator = (constraint: Constraint, context: Context) => { + const { contextName, operator} = constraint; + const value = constraint.value as string; + const contextValue = resolveContextValue(context, contextName); + if(!contextValue) { + return false; + } + + if(operator === Operator.SEMVER_EQ) { + return semverEq(contextValue, value); + } if(operator === Operator.SEMVER_LT) { + return semverLt(contextValue, value); + } if(operator === Operator.SEMVER_GT) { + return semverGt(contextValue, value); + } + return false; +} + const DateOperator = (constraint: Constraint, context: Context) => { const { operator } = constraint; const value = constraint.value as Date; @@ -119,6 +141,9 @@ operators.set(Operator.NUM_GT, NumberOperator); operators.set(Operator.NUM_GTE, NumberOperator); operators.set(Operator.DATE_AFTER, DateOperator); operators.set(Operator.DATE_BEFORE, DateOperator); +operators.set(Operator.SEMVER_EQ, SemverOperator); +operators.set(Operator.SEMVER_GT, SemverOperator); +operators.set(Operator.SEMVER_LT, SemverOperator); export class Strategy { public name: string; diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index 67b85d1f..1bd6ea92 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -411,4 +411,92 @@ test('should be disabled when date is not before', (t) => { currentTime: new Date("2022-01-27T14:00:00.000Z"), }; t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + + +test('should be enabled when semver eq', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', + operator: 'SEMVER_EQ', value: '1.2.2' }, + ]; + const context = { + environment: 'dev', + properties: { version: '1.2.2' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when semver lt', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', + operator: 'SEMVER_LT', value: '1.2.2' }, + ]; + const context = { + environment: 'dev', + properties: { version: '1.2.0' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when semver gt', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', + operator: 'SEMVER_GT', value: '1.2.2' }, + ]; + const context = { + environment: 'dev', + properties: { version: '1.2.5' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be enabled when semver in range', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', operator: 'SEMVER_GT', value: '1.2.2' }, + { contextName: 'version', operator: 'SEMVER_LT', value: '2.0.0' }, + ]; + + const context = { + environment: 'dev', + properties: { version: '1.2.5' }, + }; + t.true(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be disabled when semver out of range', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', operator: 'SEMVER_GT', value: '1.2.2' }, + { contextName: 'version', operator: 'SEMVER_LT', value: '2.0.0' }, + ]; + + const context = { + environment: 'dev', + properties: { version: '1.2.0' }, + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); +}); + +test('should be disabled when semver larger than range', (t) => { + const strategy = new Strategy('test', true); + const params = {}; + const constraints = [ + { contextName: 'version', operator: 'SEMVER_GT', value: '1.2.2' }, + { contextName: 'version', operator: 'SEMVER_LT', value: '2.0.0' }, + ]; + + const context = { + environment: 'dev', + properties: { version: '2.2.1' }, + }; + t.false(strategy.isEnabledWithConstraints(params, context, constraints)); }); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3f521404..7a64bf94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -528,6 +528,11 @@ resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== +"@types/semver@^7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/ssri@*": version "7.1.1" resolved "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.1.tgz" From 8acee599db2e50de663fde5561c5126c217cf9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 2 Feb 2022 13:25:45 +0100 Subject: [PATCH 11/12] fix: cleanup code a bit --- src/strategy/strategy.ts | 4 ++-- test/strategy/strategy-test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strategy/strategy.ts b/src/strategy/strategy.ts index f58287bf..e5ae7853 100644 --- a/src/strategy/strategy.ts +++ b/src/strategy/strategy.ts @@ -93,8 +93,8 @@ const SemverOperator = (constraint: Constraint, context: Context) => { const DateOperator = (constraint: Constraint, context: Context) => { const { operator } = constraint; - const value = constraint.value as Date; - const currentTime = context.currentTime || new Date(); + const value = new Date(constraint.value as string); + const currentTime = context.currentTime ? new Date(context.currentTime) : new Date(); if(operator === Operator.DATE_AFTER) { return currentTime > value; diff --git a/test/strategy/strategy-test.js b/test/strategy/strategy-test.js index 1bd6ea92..cab29575 100644 --- a/test/strategy/strategy-test.js +++ b/test/strategy/strategy-test.js @@ -351,7 +351,7 @@ test('should be enabled when date is after', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'DATE_AFTER', value: new Date("2022-01-29T13:00:00.000Z") }, + { contextName: 'someVal', operator: 'DATE_AFTER', value: "2022-01-29T13:00:00.000Z" }, ]; const context = { environment: 'dev', @@ -364,7 +364,7 @@ test('should be disabled when date is not after', (t) => { const strategy = new Strategy('test', true); const params = {}; const constraints = [ - { contextName: 'someVal', operator: 'DATE_AFTER', value: new Date("2022-01-29T13:00:00.000Z") }, + { contextName: 'someVal', operator: 'DATE_AFTER', value: "2022-01-29T13:00:00.000Z" }, ]; const context = { environment: 'dev', From 24ed1ed35fbf093a72f57ddbf7e3cc1ae594a1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 9 Feb 2022 22:43:01 +0100 Subject: [PATCH 12/12] fix: add updated client-specifications --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 53529472..b7259c49 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/node": "14.18.9", "@types/semver": "^7.3.9", "@typescript-eslint/eslint-plugin": "5.10.0", - "@unleash/client-specification": "4.0.0", + "@unleash/client-specification": "4.1.0", "ava": "3.15.0", "coveralls": "3.1.1", "cross-env": "7.0.3", diff --git a/yarn.lock b/yarn.lock index 7a64bf94..ec09d1c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -654,10 +654,10 @@ "@typescript-eslint/types" "5.10.0" eslint-visitor-keys "^3.0.0" -"@unleash/client-specification@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@unleash/client-specification/-/client-specification-4.0.0.tgz" - integrity sha512-K5JoOqGz8g1EYIV5oJiM3sCEKd4EHUKj1/zLq3Z2ExZ/+aQ/04kQDeJ62aTNtjErbIDH6toOBQ1BgS/1Bie0wg== +"@unleash/client-specification@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@unleash/client-specification/-/client-specification-4.1.0.tgz#14cf8b5d476ed2ef04129ae6e8c5713965f2edda" + integrity sha512-bDshwPDhUlN0JXXCCYmfAb/QdMb+2DhsqzmBs7aXHc7+5HxHaq91uSNkrVPQIBoOpQotgVODF87/HRPoKq8UiA== abbrev@1: version "1.1.1"