From 86ba6022ca47a00f7401873740f52b18f39c2e29 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 25 Jun 2025 15:39:35 +0100 Subject: [PATCH 1/5] chore(flag-evaluation): bump version to 0.1.5 and improve operator evaluations - Updated version in package.json to 0.1.5. - Refined evaluation logic for "SET" and "NOT_SET" operators to use strict equality checks. - Added tests to ensure correct behavior for missing context fields with "SET" and "NOT_SET" operators, including handling empty string values. --- packages/flag-evaluation/package.json | 2 +- packages/flag-evaluation/src/index.ts | 12 +- packages/flag-evaluation/test/index.test.ts | 122 ++++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/packages/flag-evaluation/package.json b/packages/flag-evaluation/package.json index 815f352b..0b52d043 100644 --- a/packages/flag-evaluation/package.json +++ b/packages/flag-evaluation/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/flag-evaluation", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "repository": { "type": "git", diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index e87ae2c1..e5a3dd8f 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -325,9 +325,9 @@ export function evaluate( : fieldValueDate < daysAgo.getTime(); } case "SET": - return fieldValue != ""; + return fieldValue !== ""; case "NOT_SET": - return fieldValue == ""; + return fieldValue === ""; case "IS": return fieldValue === value; case "IS_NOT": @@ -357,13 +357,17 @@ function evaluateRecursively( case "constant": return filter.value; case "context": - if (!(filter.field in context)) { + if ( + !(filter.field in context) && + filter.operator !== "SET" && + filter.operator !== "NOT_SET" + ) { missingContextFieldsSet.add(filter.field); return false; } return evaluate( - context[filter.field], + context[filter.field] ?? "", filter.operator, filter.values || [], filter.valueSet, diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 66ffa1e2..9d04d8fa 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -311,6 +311,126 @@ describe("evaluate feature targeting integration ", () => { }); }); + it("should not report missing context fields for `SET` operator when field doesn't exist", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "SET", + values: [], + }, + }, + ], + context: {}, + }); + + expect(res).toEqual({ + featureKey: "test_feature", + value: undefined, + context: {}, + ruleEvaluationResults: [false], + reason: "no matched rules", + missingContextFields: [], + }); + }); + + it("should not report missing context fields for `NOT_SET` operator when field doesn't exist", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "NOT_SET", + values: [], + }, + }, + ], + context: {}, + }); + + expect(res).toEqual({ + featureKey: "test_feature", + value: true, + context: {}, + ruleEvaluationResults: [true], + reason: "rule #0 matched", + missingContextFields: [], + }); + }); + + it("should handle `SET` operator with empty string field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "SET", + values: [], + }, + }, + ], + context: { + user: { + name: "", + }, + }, + }); + + expect(res).toEqual({ + featureKey: "test_feature", + value: undefined, + context: { + "user.name": "", + }, + ruleEvaluationResults: [false], + reason: "no matched rules", + missingContextFields: [], + }); + }); + + it("should handle `NOT_SET` operator with empty string field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "NOT_SET", + values: [], + }, + }, + ], + context: { + user: { + name: "", + }, + }, + }); + + expect(res).toEqual({ + featureKey: "test_feature", + value: true, + context: { + "user.name": "", + }, + ruleEvaluationResults: [true], + reason: "rule #0 matched", + missingContextFields: [], + }); + }); + it.each([ { context: { "company.id": "company1" }, @@ -376,6 +496,8 @@ describe("operator evaluation", () => { ["value", "SET", "", true], ["", "SET", "", false], + ["value", "NOT_SET", "", false], + ["", "NOT_SET", "", true], // non numeric values should return false ["value", "GT", "value", false], From 9250d1351ccb582235cbe1f43f3f820ba15a29e3 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 25 Jun 2025 15:40:42 +0100 Subject: [PATCH 2/5] chore(yarn): update flag-evaluation dependency resolution in yarn.lock - Adjusted the resolution for "@bucketco/flag-evaluation" to version 0.1.4. - Updated dependency details including checksum and link type. --- yarn.lock | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4d14bc76..99ea94ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,7 +768,16 @@ __metadata: languageName: unknown linkType: soft -"@bucketco/flag-evaluation@npm:0.1.4, @bucketco/flag-evaluation@workspace:packages/flag-evaluation": +"@bucketco/flag-evaluation@npm:0.1.4": + version: 0.1.4 + resolution: "@bucketco/flag-evaluation@npm:0.1.4" + dependencies: + js-sha256: "npm:0.11.0" + checksum: 10c0/d1f0513a3167d1c4d2a3caf782072f949f508760d77be2c6d5957f5a92d76aaf70467c1d923591c6663a6dcbe01f4d476e46eb501834231ae3d4f28ecff6640c + languageName: node + linkType: hard + +"@bucketco/flag-evaluation@workspace:packages/flag-evaluation": version: 0.0.0-use.local resolution: "@bucketco/flag-evaluation@workspace:packages/flag-evaluation" dependencies: From a2712200f1c4019fb8f661cb04e486a540db2c11 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 25 Jun 2025 15:48:25 +0100 Subject: [PATCH 3/5] test(flag-evaluation): enhance tests for SET and NOT_SET operators - Added tests to verify behavior of `SET` and `NOT_SET` operators when field values are missing or empty strings. - Improved clarity and organization of test cases for better readability and maintainability. --- packages/flag-evaluation/test/index.test.ts | 210 ++++++++++---------- 1 file changed, 106 insertions(+), 104 deletions(-) diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 9d04d8fa..59c4c923 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -311,123 +311,125 @@ describe("evaluate feature targeting integration ", () => { }); }); - it("should not report missing context fields for `SET` operator when field doesn't exist", () => { - const res = evaluateFeatureRules({ - featureKey: "test_feature", - rules: [ - { - value: true, - filter: { - type: "context", - field: "user.name", - operator: "SET", - values: [], + describe("SET and NOT_SET operators", () => { + it("should handle `SET` operator with missing field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "SET", + values: [], + }, }, - }, - ], - context: {}, - }); - - expect(res).toEqual({ - featureKey: "test_feature", - value: undefined, - context: {}, - ruleEvaluationResults: [false], - reason: "no matched rules", - missingContextFields: [], - }); - }); - - it("should not report missing context fields for `NOT_SET` operator when field doesn't exist", () => { - const res = evaluateFeatureRules({ - featureKey: "test_feature", - rules: [ - { - value: true, - filter: { - type: "context", - field: "user.name", - operator: "NOT_SET", - values: [], + ], + context: {}, + }); + + expect(res).toEqual({ + featureKey: "test_feature", + value: undefined, + context: {}, + ruleEvaluationResults: [false], + reason: "no matched rules", + missingContextFields: [], + }); + }); + + it("should handle `NOT_SET` operator with missing field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "NOT_SET", + values: [], + }, }, - }, - ], - context: {}, - }); + ], + context: {}, + }); - expect(res).toEqual({ - featureKey: "test_feature", - value: true, - context: {}, - ruleEvaluationResults: [true], - reason: "rule #0 matched", - missingContextFields: [], + expect(res).toEqual({ + featureKey: "test_feature", + value: true, + context: {}, + ruleEvaluationResults: [true], + reason: "rule #0 matched", + missingContextFields: [], + }); }); - }); - it("should handle `SET` operator with empty string field value", () => { - const res = evaluateFeatureRules({ - featureKey: "test_feature", - rules: [ - { - value: true, - filter: { - type: "context", - field: "user.name", - operator: "SET", - values: [], + it("should handle `SET` operator with empty string field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "SET", + values: [], + }, + }, + ], + context: { + user: { + name: "", }, }, - ], - context: { - user: { - name: "", - }, - }, - }); + }); - expect(res).toEqual({ - featureKey: "test_feature", - value: undefined, - context: { - "user.name": "", - }, - ruleEvaluationResults: [false], - reason: "no matched rules", - missingContextFields: [], + expect(res).toEqual({ + featureKey: "test_feature", + value: undefined, + context: { + "user.name": "", + }, + ruleEvaluationResults: [false], + reason: "no matched rules", + missingContextFields: [], + }); }); - }); - it("should handle `NOT_SET` operator with empty string field value", () => { - const res = evaluateFeatureRules({ - featureKey: "test_feature", - rules: [ - { - value: true, - filter: { - type: "context", - field: "user.name", - operator: "NOT_SET", - values: [], + it("should handle `NOT_SET` operator with empty string field value", () => { + const res = evaluateFeatureRules({ + featureKey: "test_feature", + rules: [ + { + value: true, + filter: { + type: "context", + field: "user.name", + operator: "NOT_SET", + values: [], + }, + }, + ], + context: { + user: { + name: "", }, }, - ], - context: { - user: { - name: "", - }, - }, - }); + }); - expect(res).toEqual({ - featureKey: "test_feature", - value: true, - context: { - "user.name": "", - }, - ruleEvaluationResults: [true], - reason: "rule #0 matched", - missingContextFields: [], + expect(res).toEqual({ + featureKey: "test_feature", + value: true, + context: { + "user.name": "", + }, + ruleEvaluationResults: [true], + reason: "rule #0 matched", + missingContextFields: [], + }); }); }); From 036a18e649e6f86fac4d15b7ad5da23b078f2ffb Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 25 Jun 2025 17:42:17 +0100 Subject: [PATCH 4/5] fix(flag-evaluation): enhance flattenJSON function to handle null and undefined values - Updated the flattenJSON function to return empty strings for null values and skip undefined values. - Improved handling of empty nested objects and arrays, ensuring consistent output. - Added comprehensive tests to cover new edge cases, including handling of top-level primitives and special characters. --- packages/flag-evaluation/src/index.ts | 46 ++++-- packages/flag-evaluation/test/index.test.ts | 148 +++++++++++++++++++- 2 files changed, 177 insertions(+), 17 deletions(-) diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index e5a3dd8f..886759b4 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -203,26 +203,44 @@ export interface Rule { * @return {Record} A flattened JSON object with "stringified" keys and values. */ export function flattenJSON(data: object): Record { - if (Object.keys(data).length === 0) return {}; - const result: Record = {}; - function recurse(cur: any, prop: string) { - if (Object(cur) !== cur) { - result[prop] = cur; - } else if (Array.isArray(cur)) { - const l = cur.length; - for (let i = 0; i < l; i++) - recurse(cur[i], prop ? prop + "." + i : "" + i); - if (l == 0) result[prop] = []; + const result: Record = {}; + + if (Object.keys(data).length === 0) { + return result; + } + + function recurse(value: any, prop: string = "") { + if (value === undefined) { + return; + } + + if (value === null) { + result[prop] = ""; + } else if (typeof value !== "object") { + result[prop] = String(value); + } else if (Array.isArray(value)) { + if (value.length === 0) { + result[prop] = ""; + } + + for (let i = 0; i < value.length; i++) { + recurse(value[i], prop ? prop + "." + i : "" + i); + } } else { let isEmpty = true; - for (const p in cur) { + + for (const p in value) { isEmpty = false; - recurse(cur[p], prop ? prop + "." + p : p); + recurse(value[p], prop ? prop + "." + p : p); + } + + if (isEmpty) { + result[prop] = ""; } - if (isEmpty) result[prop] = {}; } } - recurse(data, ""); + + recurse(data); return result; } diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 59c4c923..5434c0ee 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -650,8 +650,8 @@ describe("flattenJSON", () => { expect(output).toEqual({ "a.b": "string", - "a.c": 123, - "a.d": true, + "a.c": "123", + "a.d": "true", }); }); @@ -677,7 +677,7 @@ describe("flattenJSON", () => { const output = flattenJSON(input); expect(output).toEqual({ - a: [], + a: "", }); }); @@ -732,6 +732,148 @@ describe("flattenJSON", () => { "a.b": "", }); }); + + it("should handle null values", () => { + const input = { + a: null, + b: { + c: null, + }, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + a: "", + "b.c": "", + }); + }); + + it("should skip undefined values", () => { + const input = { + a: "value", + b: undefined, + c: { + d: undefined, + e: "another value", + }, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + a: "value", + "c.e": "another value", + }); + }); + + it("should handle empty nested objects", () => { + const input = { + a: {}, + b: { + c: {}, + d: "value", + }, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + a: "", + "b.c": "", + "b.d": "value", + }); + }); + + it("should handle top-level primitive values", () => { + const input = { + a: "simple", + b: 42, + c: true, + d: false, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + a: "simple", + b: "42", + c: "true", + d: "false", + }); + }); + + it("should handle arrays with null and undefined values", () => { + const input = { + a: ["value1", null, undefined, "value4"], + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + "a.0": "value1", + "a.1": "", + "a.3": "value4", + }); + }); + + it("should handle deeply nested empty structures", () => { + const input = { + a: { + b: { + c: {}, + d: [], + }, + }, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + "a.b.c": "", + "a.b.d": "", + }); + }); + + it("should handle keys with special characters", () => { + const input = { + "key.with.dots": "value1", + "key-with-dashes": "value2", + "key with spaces": "value3", + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + "key.with.dots": "value1", + "key-with-dashes": "value2", + "key with spaces": "value3", + }); + }); + + it("should handle edge case numbers and booleans", () => { + const input = { + zero: 0, + negativeNumber: -42, + float: 3.14, + infinity: Infinity, + negativeInfinity: -Infinity, + nan: NaN, + falseValue: false, + }; + + const output = flattenJSON(input); + + expect(output).toEqual({ + zero: "0", + negativeNumber: "-42", + float: "3.14", + infinity: "Infinity", + negativeInfinity: "-Infinity", + nan: "NaN", + falseValue: "false", + }); + }); }); describe("unflattenJSON", () => { From f87e86bdc3e21541a2c1a7ca5b8c920e0149d1e4 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 26 Jun 2025 17:24:30 +0100 Subject: [PATCH 5/5] refactor(flag-evaluation): improve recurse function in flattenJSON - Updated the recurse function in flattenJSON to require a prop parameter, enhancing type safety and clarity. - Ensured consistent handling of input data by explicitly passing an empty string for the initial prop value. --- packages/flag-evaluation/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index 886759b4..51488f7e 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -209,7 +209,7 @@ export function flattenJSON(data: object): Record { return result; } - function recurse(value: any, prop: string = "") { + function recurse(value: any, prop: string) { if (value === undefined) { return; } @@ -240,7 +240,7 @@ export function flattenJSON(data: object): Record { } } - recurse(data); + recurse(data, ""); return result; }