diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 8dc854e..fabd9a5 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -1,3 +1,5 @@ +import semver from 'semver'; + import { FeatureStateModel } from '../features/models'; import { getCastingFunction as getCastingFunction } from '../utils'; import { @@ -8,6 +10,7 @@ import { REGEX, CONDITION_OPERATORS } from './constants'; +import { isSemver } from './util'; export const all = (iterable: Array) => iterable.filter(e => !!e).length === iterable.length; export const any = (iterable: Array) => iterable.filter(e => !!e).length > 0; @@ -25,6 +28,19 @@ export const matchingFunctions = { otherValue.includes(thisValue), }; +export const semverMatchingFunction = { + ...matchingFunctions, + [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => semver.eq(thisValue, otherValue), + [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => semver.gt(otherValue, thisValue), + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => + semver.gte(otherValue, thisValue), + [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => semver.gt(thisValue, otherValue), + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => + semver.gte(thisValue, otherValue), +} + +export const getMatchingFunctions = (semver: boolean) => (semver ? semverMatchingFunction : matchingFunctions); + export class SegmentConditionModel { EXCEPTION_OPERATOR_METHODS: { [key: string]: string } = { [NOT_CONTAINS]: 'evaluateNotContains', @@ -59,9 +75,12 @@ export class SegmentConditionModel { const defaultFunction = (x: any, y: any) => false; - const matchingFunction = matchingFunctions[this.operator] || defaultFunction; + const matchingFunctionSet = getMatchingFunctions(isSemver(this.value)); + const matchingFunction = matchingFunctionSet[this.operator] || defaultFunction; + + const traitType = isSemver(this.value) ? 'semver' : typeof traitValue; + const castToTypeOfTraitValue = getCastingFunction(traitType); - const castToTypeOfTraitValue = getCastingFunction(traitValue); return matchingFunction(castToTypeOfTraitValue(this.value), traitValue); } } diff --git a/flagsmith-engine/segments/util.ts b/flagsmith-engine/segments/util.ts index aa939bf..307964a 100644 --- a/flagsmith-engine/segments/util.ts +++ b/flagsmith-engine/segments/util.ts @@ -27,3 +27,11 @@ export function buildSegmentModel(segmentModelJSON: any): SegmentModel { return model; } + +export function isSemver(value: any) { + return typeof value == 'string' && value.endsWith(':semver'); +} + +export function removeSemverSuffix(value: string) { + return value.replace(':semver', ''); +} \ No newline at end of file diff --git a/flagsmith-engine/utils/index.ts b/flagsmith-engine/utils/index.ts index f4bd133..c77f6ed 100644 --- a/flagsmith-engine/utils/index.ts +++ b/flagsmith-engine/utils/index.ts @@ -1,11 +1,13 @@ -import { FeatureSegment } from "../features/models"; +import { removeSemverSuffix } from "../segments/util"; -export function getCastingFunction(input: any): CallableFunction { - switch (typeof input) { +export function getCastingFunction(traitType: 'boolean' | 'string' | 'number' | 'semver' | any): CallableFunction { + switch (traitType) { case 'boolean': return (x: any) => !['False', 'false'].includes(x); case 'number': return (x: any) => parseFloat(x); + case 'semver': + return (x: any) => removeSemverSuffix(x); default: return (x: any) => String(x); } diff --git a/package-lock.json b/package-lock.json index c7487fb..7277a25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,14 @@ "big-integer": "^1.6.51", "md5": "^2.3.0", "node-fetch": "^2.1.2", + "semver": "^7.3.7", "uuid": "^8.3.2" }, "devDependencies": { "@types/jest": "^27.4.1", "@types/md5": "^2.3.2", "@types/node-fetch": "^2.6.1", + "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "esbuild": "^0.14.25", "husky": "^7.0.4", @@ -90,6 +92,15 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.17.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", @@ -131,6 +142,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-environment-visitor": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", @@ -1019,6 +1039,12 @@ "integrity": "sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -2572,6 +2598,15 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -3133,21 +3168,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-util": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", @@ -3404,7 +3424,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3427,6 +3446,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3928,12 +3956,17 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -4260,21 +4293,6 @@ } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -4542,8 +4560,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "16.2.0", @@ -4619,6 +4636,14 @@ "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/generator": { @@ -4650,6 +4675,14 @@ "@babel/helper-validator-option": "^7.16.7", "browserslist": "^4.17.5", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/helper-environment-visitor": { @@ -5367,6 +5400,12 @@ "integrity": "sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==", "dev": true }, + "@types/semver": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -6439,6 +6478,14 @@ "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "istanbul-lib-report": { @@ -6877,17 +6924,6 @@ "natural-compare": "^1.4.0", "pretty-format": "^27.5.1", "semver": "^7.3.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "jest-util": { @@ -7088,7 +7124,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -7100,6 +7135,14 @@ "dev": true, "requires": { "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "make-error": { @@ -7482,10 +7525,12 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "requires": { + "lru-cache": "^6.0.0" + } }, "shebang-command": { "version": "2.0.0", @@ -7712,17 +7757,6 @@ "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "type-check": { @@ -7922,8 +7956,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index c5d0d02..ff01b04 100644 --- a/package.json +++ b/package.json @@ -53,12 +53,14 @@ "big-integer": "^1.6.51", "md5": "^2.3.0", "node-fetch": "^2.1.2", + "semver": "^7.3.7", "uuid": "^8.3.2" }, "devDependencies": { "@types/jest": "^27.4.1", "@types/md5": "^2.3.2", "@types/node-fetch": "^2.6.1", + "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "esbuild": "^0.14.25", "husky": "^7.0.4", @@ -67,4 +69,4 @@ "ts-jest": "^27.1.3", "typescript": "^4.6.2" } -} \ No newline at end of file +} diff --git a/tests/engine/unit/segments/segments_model.test.ts b/tests/engine/unit/segments/segments_model.test.ts index a9e00d5..09f9f8b 100644 --- a/tests/engine/unit/segments/segments_model.test.ts +++ b/tests/engine/unit/segments/segments_model.test.ts @@ -62,6 +62,26 @@ const conditionMatchCases: [string, string | number | boolean, string, boolean][ [CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'baz', true], [CONDITION_OPERATORS.REGEX, 'foo', '[a-z]+', true], [CONDITION_OPERATORS.REGEX, 'FOO', '[a-z]+', false], + [CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.0:semver", true], + [CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.0:semver", true], + [CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.1:semver", false], + [CONDITION_OPERATORS.NOT_EQUAL, "1.0.0", "1.0.0:semver", false], + [CONDITION_OPERATORS.NOT_EQUAL, "1.0.0", "1.0.1:semver", true], + [CONDITION_OPERATORS.GREATER_THAN, "1.0.1", "1.0.0:semver", true], + [CONDITION_OPERATORS.GREATER_THAN, "1.0.0", "1.0.0-beta:semver", true], + [CONDITION_OPERATORS.GREATER_THAN, "1.0.1", "1.2.0:semver", false], + [CONDITION_OPERATORS.GREATER_THAN, "1.0.1", "1.0.1:semver", false], + [CONDITION_OPERATORS.GREATER_THAN, "1.2.4", "1.2.3-pre.2+build.4:semver", true], + [CONDITION_OPERATORS.LESS_THAN, "1.0.0", "1.0.1:semver", true], + [CONDITION_OPERATORS.LESS_THAN, "1.0.0", "1.0.0:semver", false], + [CONDITION_OPERATORS.LESS_THAN, "1.0.1", "1.0.0:semver", false], + [CONDITION_OPERATORS.LESS_THAN, "1.0.0-rc.2", "1.0.0-rc.3:semver", true], + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", true], + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, "1.0.1", "1.2.0:semver", false], + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.1:semver", true], + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.1:semver", true], + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.0:semver", true], + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", false], ['BAD_OP', 'a', 'a', false] ];