From d6caa683a9c3e82c1f147988f646f14d25f9e1c2 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 18 Jul 2025 19:19:49 -0300 Subject: [PATCH 1/3] [FME-7164] update swagger api-docs --- environmentManager/__tests__/manager.test.js | 20 ++++--- openapi/openapi.yaml | 56 +++++++++++++++---- ...itchanges.since.-1.till.1602796638344.json | 5 ++ 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/environmentManager/__tests__/manager.test.js b/environmentManager/__tests__/manager.test.js index 4d8731f..a72504c 100644 --- a/environmentManager/__tests__/manager.test.js +++ b/environmentManager/__tests__/manager.test.js @@ -61,12 +61,12 @@ describe('environmentManager - manager endpoints', () => { .get('/manager/splits') .set('Authorization', 'key_green'); expect(response.statusCode).toBe(200); - expect(response.body.splits.map(flag => {return {name: flag.name, prerequisites: flag.prerequisites};})) + expect(response.body.splits.map(flag => {return {name: flag.name, prerequisites: flag.prerequisites, impressionsDisabled: flag.impressionsDisabled};})) .toEqual( [ - {name: 'test_green', prerequisites: [{flagName: 'flag1', treatments: ['on', 'v1']}, {flagName: 'flag2', treatments: ['off']}]}, - {name: 'test_color', prerequisites: []}, - {name: 'test_green_config', prerequisites: [{flagName: 'flag3', treatments: ['on', 'v2']}, {flagName: 'flag4', treatments: ['off']}, {flagName: 'flag5', treatments: ['off']}]} + {name: 'test_green', prerequisites: [{flagName: 'flag1', treatments: ['on', 'v1']}, {flagName: 'flag2', treatments: ['off']}], impressionsDisabled: true}, + {name: 'test_color', prerequisites: [], impressionsDisabled: false}, + {name: 'test_green_config', prerequisites: [{flagName: 'flag3', treatments: ['on', 'v2']}, {flagName: 'flag4', treatments: ['off']}, {flagName: 'flag5', treatments: ['off']}], impressionsDisabled: true} ] ); }); @@ -76,12 +76,12 @@ describe('environmentManager - manager endpoints', () => { .get('/manager/splits') .set('Authorization', 'key_purple'); expect(response.statusCode).toBe(200); - expect(response.body.splits.map(flag => {return {name: flag.name, prerequisites: flag.prerequisites};})) + expect(response.body.splits.map(flag => {return {name: flag.name, prerequisites: flag.prerequisites, impressionsDisabled: flag.impressionsDisabled};})) .toEqual( [ - {name: 'test_color', prerequisites: []}, - {name: 'test_purple', prerequisites: []}, - {name: 'test_purple_config', prerequisites: []} + {name: 'test_color', prerequisites: [], impressionsDisabled: false}, + {name: 'test_purple', prerequisites: [], impressionsDisabled: false}, + {name: 'test_purple_config', prerequisites: [], impressionsDisabled: true} ] ); }); @@ -130,6 +130,7 @@ describe('environmentManager - manager endpoints', () => { expect(response.body.name).toEqual('test_green'); expect(response.body.sets).toEqual(['set_green']); expect(response.body.prerequisites).toEqual([{flagName: 'flag1', treatments: ['on', 'v1']}, {flagName: 'flag2', treatments: ['off']}]); + expect(response.body.impressionsDisabled).toEqual(true); }); test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_purple', async () => { @@ -140,6 +141,7 @@ describe('environmentManager - manager endpoints', () => { expect(response.body.name).toEqual('test_purple'); expect(response.body.sets).toEqual(['set_purple']); expect(response.body.prerequisites).toEqual([]); + expect(response.body.impressionsDisabled).toEqual(false); }); test('[/split] should be 404 if is valid authToken and return 404 for test_green using key_purple', async () => { @@ -157,6 +159,7 @@ describe('environmentManager - manager endpoints', () => { expect(response.body.name).toEqual('test_green'); expect(response.body.sets).toEqual(['set_green']); expect(response.body.prerequisites).toEqual([{flagName: 'flag1', treatments: ['on', 'v1']}, {flagName: 'flag2', treatments: ['off']}]); + expect(response.body.impressionsDisabled).toEqual(true); }); test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_pink', async () => { @@ -167,6 +170,7 @@ describe('environmentManager - manager endpoints', () => { expect(response.body.name).toEqual('test_purple'); expect(response.body.sets).toEqual(['set_purple']); expect(response.body.prerequisites).toEqual([]); + expect(response.body.impressionsDisabled).toEqual(false); }); diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 1ad2fe9..6d0b6d1 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -61,6 +61,13 @@ components: type: string description: A JSON string of the attributes to include in the evaluation. example: "{\"my-attr1\": \"test\"}" + options: + in: query + name: options + schema: + type: string + description: A JSON string of the options to include in the evaluation. + example: "{\"properties\": {\"package\":\"premium\",\"admin\":true,\"discount\":50}}" keys: in: query name: keys @@ -101,7 +108,7 @@ components: example: "{\"prop1\": 1}" requestBodies: EvaluationRequestBody: - description: A JSON of the attributes to include in the evaluation. + description: A JSON of the attributes and options to include in the evaluation. required: false content: application/json: @@ -113,7 +120,7 @@ components: properties: attributes: type: object - example: { attributes: { myattr1: 'test' }} + example: { attributes: { myattr1: 'test' }, properties: {"package":"premium","admin":true,"discount":50}} InputValidation: type: object properties: @@ -159,6 +166,21 @@ components: type: string configs: type: object + impressionsDisabled: + type: boolean + description: Indicates if impressions are disabled for this split. + prerequisites: + type: array + description: List of prerequisites for this split. + items: + type: object + properties: + flagName: + type: string + treatments: + type: array + items: + type: string example: name: my-split trafficType: my-traffic-type @@ -168,6 +190,12 @@ components: config: on: "{\"color\": \"blue\"}" off: "{\"color\": \"black\"}" + impressionsDisabled: false + prerequisites: + - flagName: flag1 + treatments: ["on", "v1"] + - flagName: flag2 + treatments: ["off"] Splits: type: object properties: @@ -191,6 +219,7 @@ paths: - $ref: '#/components/parameters/split-name' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -223,7 +252,7 @@ paths: post: tags: - client - summary: performs single evaluation but receiving the attributes on the request body. + summary: performs single evaluation but receiving the attributes and options on the request body. description: Calls getTreatment method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -271,6 +300,7 @@ paths: - $ref: '#/components/parameters/split-name' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -306,7 +336,7 @@ paths: post: tags: - client - summary: performs single evaluation and attachs config, but receiving the attributes on the request body. + summary: performs single evaluation and attachs config, but receiving the attributes and options on the request body. description: Calls getTreatmentWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -357,6 +387,7 @@ paths: - $ref: '#/components/parameters/split-names' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -386,7 +417,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once, but receiving the attributes on the request body. + summary: performs multiple evaluation at once, but receiving the attributes and options on the request body. description: Calls getTreatments method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -431,6 +462,7 @@ paths: - $ref: '#/components/parameters/split-names' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -462,7 +494,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once and attachs config, but receiving the attributes on the request body. + summary: performs multiple evaluation at once and attachs config, but receiving the attributes and options on the request body. description: Calls getTreatmentsWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -509,6 +541,7 @@ paths: - $ref: '#/components/parameters/flag-sets' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -538,7 +571,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once by sets, but receiving the attributes on the request body. + summary: performs multiple evaluation at once by sets, but receiving the attributes and options on the request body. description: Calls getTreatmentsByFlagSets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -583,6 +616,7 @@ paths: - $ref: '#/components/parameters/flag-sets' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -614,7 +648,7 @@ paths: post: tags: - client - summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes on the request body. + summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes and options on the request body. description: Calls getTreatmentsWithConfigBySets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -659,6 +693,7 @@ paths: parameters: - $ref: '#/components/parameters/keys' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -701,7 +736,7 @@ paths: post: tags: - client - summary: matches feature flags for passed trafficType and evaluates with passed key, but receiving the attributes on the request body. + summary: matches feature flags for passed trafficType and evaluates with passed key, but receiving the attributes and options on the request body. description: Calls getTreatments method from the SDK once it have all the splitNames for the keys provided. parameters: - $ref: '#/components/parameters/keys' @@ -755,6 +790,7 @@ paths: parameters: - $ref: '#/components/parameters/keys' - $ref: '#/components/parameters/attributes' + - $ref: '#/components/parameters/options' responses: '200': description: Evaluation result @@ -801,7 +837,7 @@ paths: post: tags: - client - summary: matches feature flags for passed trafficType and evaluates with passed key and attaches configs, but receiving the attributes on the request body. + summary: matches feature flags for passed trafficType and evaluates with passed key and attaches configs, but receiving the attributes and options on the request body. description: Calls getTreatmentsWithConfig method from the SDK once it have all the splitNames for the keys provided. parameters: - $ref: '#/components/parameters/keys' diff --git a/utils/mocks/splitchanges.since.-1.till.1602796638344.json b/utils/mocks/splitchanges.since.-1.till.1602796638344.json index 24de58c..701c952 100644 --- a/utils/mocks/splitchanges.since.-1.till.1602796638344.json +++ b/utils/mocks/splitchanges.since.-1.till.1602796638344.json @@ -43,6 +43,7 @@ "label": "default rule" } ], + "impressionsDisabled": true, "prerequisites": [ { "n": "flag1", "ts": ["on","v1"] }, { "n": "flag2", "ts": ["off"] } @@ -90,6 +91,7 @@ "label": "default rule" } ], + "impressionsDisabled": false, "prerequisites": [] }, { @@ -136,6 +138,7 @@ "label": "default rule" } ], + "impressionsDisabled": true, "prerequisites": [ { "n": "flag3", "ts": ["on","v2"] }, { "n": "flag4", "ts": ["off"] }, @@ -184,6 +187,7 @@ "label": "default rule" } ], + "impressionsDisabled": false, "prerequisites": [] }, { @@ -230,6 +234,7 @@ "label": "default rule" } ], + "impressionsDisabled": true, "prerequisites": [] } ], From e8b24b6dab1a29853ccdca2a1679b854aaa266c5 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 21 Jul 2025 21:03:21 -0300 Subject: [PATCH 2/3] [FME-4363] Use properties param instead of options --- client/__tests__/allTreatments.test.js | 25 +++- .../__tests__/allTreatmentsWithConfig.test.js | 25 +++- client/__tests__/track.test.js | 12 +- client/__tests__/treatment.test.js | 25 +++- client/__tests__/treatmentWithConfig.test.js | 25 +++- client/__tests__/treatments.test.js | 33 +++++- client/__tests__/treatmentsByFlagSets.test.js | 25 +++- client/__tests__/treatmentsWithConfig.test.js | 35 +++++- .../treatmentsWithConfigByFlagSets.test.js | 34 ++++++ client/client.controller.js | 36 +++--- client/client.router.js | 49 ++++---- openapi/openapi.yaml | 46 ++++---- utils/constants.js | 5 + .../__tests__/properties.test.js | 107 ++++++------------ utils/inputValidation/properties.js | 34 ++---- 15 files changed, 321 insertions(+), 195 deletions(-) diff --git a/client/__tests__/allTreatments.test.js b/client/__tests__/allTreatments.test.js index db25b55..436cd93 100644 --- a/client/__tests__/allTreatments.test.js +++ b/client/__tests__/allTreatments.test.js @@ -260,18 +260,35 @@ describe('get-all-treatments', () => { expectOkAllTreatments(response, 200, expected, 2); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-all-treatments?keys=[{"matchingKey":"test","trafficType":"localhost"}]&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-all-treatments?keys=[{"matchingKey":"test","trafficType":"localhost"}]&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expect(response.status).toBe(200); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-all-treatments?keys=[{"matchingKey":"test","trafficType":"localhost"}]') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-all-treatments?keys=[{"matchingKey":"test","trafficType":"localhost"}]&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-all-treatments?keys=[{"matchingKey":"test","trafficType":"localhost"}]') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expect(response.status).toBe(200); diff --git a/client/__tests__/allTreatmentsWithConfig.test.js b/client/__tests__/allTreatmentsWithConfig.test.js index 70c8b8f..fa0581f 100644 --- a/client/__tests__/allTreatmentsWithConfig.test.js +++ b/client/__tests__/allTreatmentsWithConfig.test.js @@ -268,18 +268,35 @@ describe('get-all-treatments-with-config', () => { expectOkAllTreatments(response, 200, expected, 2); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-all-treatments-with-config?keys=[{"matchingKey":"test","trafficType":"localhost"}]&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-all-treatments-with-config?keys=[{"matchingKey":"test","trafficType":"localhost"}]&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expect(response.status).toBe(200); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-all-treatments-with-config?keys=[{"matchingKey":"test","trafficType":"localhost"}]') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-all-treatments-with-config?keys=[{"matchingKey":"test","trafficType":"localhost"}]&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-all-treatments-with-config?keys=[{"matchingKey":"test","trafficType":"localhost"}]') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expect(response.status).toBe(200); diff --git a/client/__tests__/track.test.js b/client/__tests__/track.test.js index a822a03..e500b63 100644 --- a/client/__tests__/track.test.js +++ b/client/__tests__/track.test.js @@ -4,6 +4,7 @@ process.env.SPLIT_EVALUATOR_API_KEY = 'localhost'; const request = require('supertest'); const app = require('../../app'); const { expectError, expectErrorContaining, getLongKey } = require('../../utils/testWrapper'); +const { PROPERTIES_WARNING } = require('../../utils/constants'); describe('track', () => { // Testing authorization @@ -143,14 +144,14 @@ describe('track', () => { expectErrorContaining(response, 400, expected); }); - test('should be 400 if properties is invalid', async () => { - const expected = [ - 'properties must be a plain object with only boolean, string, number or null values.' - ]; + test('should be 200 if properties is invalid', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const response = await request(app) .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=1&properties=lalala') .set('Authorization', 'test'); - expectErrorContaining(response, 400, expected); + expect(response.statusCode).toBe(200); + expect(logSpy).toHaveBeenCalledWith(PROPERTIES_WARNING); + logSpy.mockRestore(); }); test('should be 400 if there are multiple errors in every input', async () => { @@ -158,7 +159,6 @@ describe('track', () => { 'key too long, key must be 250 characters or less.', 'you passed "@!test", event-type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.', 'you passed an empty traffic-type, traffic-type must be a non-empty string.', - 'properties must be a plain object with only boolean, string, number or null values.', 'value must be null or number.' ]; const key = getLongKey(); diff --git a/client/__tests__/treatment.test.js b/client/__tests__/treatment.test.js index 0d4ec17..eab1bbb 100644 --- a/client/__tests__/treatment.test.js +++ b/client/__tests__/treatment.test.js @@ -248,18 +248,35 @@ describe('get-treatment', () => { expectOk(response, 200, 'control', 'nonexistant-experiment'); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-treatment?key=test&split-name=my-experiment&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-treatment?key=test&split-name=my-experiment&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expectOk(response, 200, 'on', 'my-experiment'); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-treatment?key=test&split-name=my-experiment') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expectOk(response, 200, 'on', 'my-experiment'); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatment?key=test&split-name=my-experiment&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expectOk(response, 200, 'on', 'my-experiment'); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatment?key=test&split-name=my-experiment') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expectOk(response, 200, 'on', 'my-experiment'); diff --git a/client/__tests__/treatmentWithConfig.test.js b/client/__tests__/treatmentWithConfig.test.js index 5af341e..1e1765a 100644 --- a/client/__tests__/treatmentWithConfig.test.js +++ b/client/__tests__/treatmentWithConfig.test.js @@ -230,18 +230,35 @@ describe('get-treatment-with-config', () => { expectOk(response, 200, 'control', 'nonexistant-experiment', null); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-treatment-with-config?key=test&split-name=my-experiment&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-treatment-with-config?key=test&split-name=my-experiment&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expectOk(response, 200, 'on', 'my-experiment', '{"desc" : "this applies only to ON treatment"}'); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-treatment-with-config?key=test&split-name=my-experiment') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expectOk(response, 200, 'on', 'my-experiment', '{"desc" : "this applies only to ON treatment"}'); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatment-with-config?key=test&split-name=my-experiment&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expectOk(response, 200, 'on', 'my-experiment', '{"desc" : "this applies only to ON treatment"}'); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatment-with-config?key=test&split-name=my-experiment') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expectOk(response, 200, 'on', 'my-experiment', '{"desc" : "this applies only to ON treatment"}'); diff --git a/client/__tests__/treatments.test.js b/client/__tests__/treatments.test.js index d6621ae..aecaac4 100644 --- a/client/__tests__/treatments.test.js +++ b/client/__tests__/treatments.test.js @@ -268,9 +268,9 @@ describe('get-treatments', () => { }, 3); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-treatments?key=test&split-names=my-experiment&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-treatments?key=test&split-names=my-experiment&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expectOkMultipleResults(response, 200, { 'my-experiment': { @@ -279,11 +279,36 @@ describe('get-treatments', () => { }, 1); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-treatments?key=test&split-names=my-experiment') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expectOkMultipleResults(response, 200, { + 'my-experiment': { + treatment: 'on', + }, + }, 1); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatments?key=test&split-names=my-experiment&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expectOkMultipleResults(response, 200, { + 'my-experiment': { + treatment: 'on', + }, + }, 1); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatments?key=test&split-names=my-experiment') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expectOkMultipleResults(response, 200, { diff --git a/client/__tests__/treatmentsByFlagSets.test.js b/client/__tests__/treatmentsByFlagSets.test.js index 0fdb1bf..3edf699 100644 --- a/client/__tests__/treatmentsByFlagSets.test.js +++ b/client/__tests__/treatmentsByFlagSets.test.js @@ -274,18 +274,35 @@ describe('get-treatments-by-sets', () => { expectOkMultipleResults(response, 200, expectedPinkResults, 5); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-treatments-by-sets?key=test&flag-sets=set_green&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-treatments-by-sets?key=test&flag-sets=set_green&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'key_green'); expect(response.status).toBe(200); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-treatments-by-sets?key=test&flag-sets=set_green') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets=set_green&properties={"foo": {"bar": 1}}') + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatments-by-sets?key=test&flag-sets=set_green') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'key_green'); expect(response.status).toBe(200); diff --git a/client/__tests__/treatmentsWithConfig.test.js b/client/__tests__/treatmentsWithConfig.test.js index d30037d..253582f 100644 --- a/client/__tests__/treatmentsWithConfig.test.js +++ b/client/__tests__/treatmentsWithConfig.test.js @@ -253,9 +253,9 @@ describe('get-treatments-with-config', () => { }, 3); }); - test('should be 200 if options.properties is valid (GET)', async () => { + test('should be 200 if properties is valid (GET)', async () => { const response = await request(app) - .get('/client/get-treatments-with-config?key=test&split-names=my-experiment&options={"properties":{"package":"premium","admin":true,"discount":50}}') + .get('/client/get-treatments-with-config?key=test&split-names=my-experiment&properties={"package":"premium","admin":true,"discount":50}') .set('Authorization', 'test'); expectOkMultipleResults(response, 200, { 'my-experiment': { @@ -265,11 +265,38 @@ describe('get-treatments-with-config', () => { }, 1); }); - test('should be 200 if options.properties is valid (POST)', async () => { + test('should be 200 if properties is valid (POST)', async () => { const response = await request(app) .post('/client/get-treatments-with-config?key=test&split-names=my-experiment') .send({ - options: { properties: { package: 'premium', admin: true, discount: 50 } }, + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'test'); + expectOkMultipleResults(response, 200, { + 'my-experiment': { + treatment: 'on', + config: '{"desc" : "this applies only to ON treatment"}', + }, + }, 1); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatments-with-config?key=test&split-names=my-experiment&properties={"foo": {"bar": 1}}') + .set('Authorization', 'test'); + expectOkMultipleResults(response, 200, { + 'my-experiment': { + treatment: 'on', + config: '{"desc" : "this applies only to ON treatment"}', + }, + }, 1); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatments-with-config?key=test&split-names=my-experiment') + .send({ + properties: { foo: { bar: 1 } }, }) .set('Authorization', 'test'); expectOkMultipleResults(response, 200, { diff --git a/client/__tests__/treatmentsWithConfigByFlagSets.test.js b/client/__tests__/treatmentsWithConfigByFlagSets.test.js index 13da4c5..fc67842 100644 --- a/client/__tests__/treatmentsWithConfigByFlagSets.test.js +++ b/client/__tests__/treatmentsWithConfigByFlagSets.test.js @@ -249,4 +249,38 @@ describe('get-treatments-with-config-by-sets', () => { .set('Authorization', 'key_pink'); expectOkMultipleResults(response, 200, expectedPinkResultsWithConfig, 5); }); + + test('should be 200 if properties is valid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=set_green&properties={"package":"premium","admin":true,"discount":50}') + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is valid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatments-with-config-by-sets?key=test&flag-sets=set_green') + .send({ + properties: { package: 'premium', admin: true, discount: 50 }, + }) + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (GET)', async () => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=set_green&properties={"foo": {"bar": 1}}') + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); + + test('should be 200 if properties is invalid (POST)', async () => { + const response = await request(app) + .post('/client/get-treatments-with-config-by-sets?key=test&flag-sets=set_green') + .send({ + properties: { foo: { bar: 1 } }, + }) + .set('Authorization', 'key_green'); + expect(response.status).toBe(200); + }); }); diff --git a/client/client.controller.js b/client/client.controller.js index 93e0502..9132b69 100644 --- a/client/client.controller.js +++ b/client/client.controller.js @@ -12,10 +12,10 @@ const getTreatment = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const featureFlag = req.splitio.featureFlagName; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResult = await client.getTreatment(key, featureFlag, attributes, evaluationOptions); + const evaluationResult = await client.getTreatment(key, featureFlag, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); res.send({ @@ -37,11 +37,11 @@ const getTreatmentWithConfig = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const featureFlag = req.splitio.featureFlagName; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResult = await client.getTreatmentWithConfig(key, featureFlag, attributes, evaluationOptions); + const evaluationResult = await client.getTreatmentWithConfig(key, featureFlag, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); res.send({ @@ -64,10 +64,10 @@ const getTreatments = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const featureFlags = req.splitio.featureFlagNames; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResults = await client.getTreatments(key, featureFlags, attributes, evaluationOptions); + const evaluationResults = await client.getTreatments(key, featureFlags, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); const result = {}; @@ -93,10 +93,10 @@ const getTreatmentsWithConfig = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const featureFlags = req.splitio.featureFlagNames; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResults = await client.getTreatmentsWithConfig(key, featureFlags, attributes, evaluationOptions); + const evaluationResults = await client.getTreatmentsWithConfig(key, featureFlags, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); res.send(evaluationResults); @@ -115,10 +115,10 @@ const getTreatmentsByFlagSets = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const flagSets = req.splitio.flagSetNames; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResults = await client.getTreatmentsByFlagSets(key, flagSets, attributes, evaluationOptions); + const evaluationResults = await client.getTreatmentsByFlagSets(key, flagSets, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); const result = {}; @@ -144,10 +144,10 @@ const getTreatmentsWithConfigByFlagSets = async (req, res) => { const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); const flagSets = req.splitio.flagSetNames; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const evaluationResults = await client.getTreatmentsWithConfigByFlagSets(key, flagSets, attributes, evaluationOptions); + const evaluationResults = await client.getTreatmentsWithConfigByFlagSets(key, flagSets, attributes, { properties }); environmentManager.updateLastEvaluation(req.headers.authorization); const result = evaluationResults; @@ -185,7 +185,7 @@ const track = async (req, res) => { * @param {Object} attributes * @param {Object} evaluationOptions */ -const allTreatments = async (authorization, keys, attributes, evaluationOptions) => { +const allTreatments = async (authorization, keys, attributes, properties) => { const manager = environmentManager.getManager(authorization); const client = environmentManager.getClient(authorization); try { @@ -201,7 +201,7 @@ const allTreatments = async (authorization, keys, attributes, evaluationOptions) parseKey(key.matchingKey, key.bucketingKey), featureFlagNames, attributes, - evaluationOptions + { properties } ); // Saves result for each trafficType evaluations[key.trafficType] = evaluation; @@ -222,10 +222,10 @@ const allTreatments = async (authorization, keys, attributes, evaluationOptions) const getAllTreatmentsWithConfig = async (req, res) => { const keys = req.splitio.keys; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const treatments = await allTreatments(req.headers.authorization, keys, attributes, evaluationOptions); + const treatments = await allTreatments(req.headers.authorization, keys, attributes, properties); environmentManager.updateLastEvaluation(req.headers.authorization); res.send(treatments); } catch (error) { @@ -241,10 +241,10 @@ const getAllTreatmentsWithConfig = async (req, res) => { const getAllTreatments = async (req, res) => { const keys = req.splitio.keys; const attributes = req.splitio.attributes; - const evaluationOptions = req.splitio.options; + const properties = req.splitio.properties; try { - const treatments = await allTreatments(req.headers.authorization, keys, attributes, evaluationOptions); + const treatments = await allTreatments(req.headers.authorization, keys, attributes, properties); // Erases the config property for treatments const trafficTypes = Object.keys(treatments); trafficTypes.forEach(trafficType => { diff --git a/client/client.router.js b/client/client.router.js index f90139f..1b97ae2 100644 --- a/client/client.router.js +++ b/client/client.router.js @@ -8,7 +8,7 @@ const attributesValidator = require('../utils/inputValidation/attributes'); const trafficTypeValidator = require('../utils/inputValidation/trafficType'); const eventTypeValidator = require('../utils/inputValidation/eventType'); const valueValidator = require('../utils/inputValidation/value'); -const { validateProperties, validateEvaluationOptions } = require('../utils/inputValidation/properties'); +const { validateProperties } = require('../utils/inputValidation/properties'); const keysValidator = require('../utils/inputValidation/keys'); const clientController = require('./client.controller'); const { parseValidators } = require('../utils/utils'); @@ -24,9 +24,9 @@ const treatmentValidation = (req, res, next) => { const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; const featureFlagNameValidation = splitValidator(req.query['split-name']); const attributesValidation = attributesValidator(req.query.attributes); - const optionsValidation = validateEvaluationOptions(req.query.options); + const propertiesValidation = validateProperties(req.query.properties, req.method === 'GET'); - const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagNameValidation, attributesValidation, optionsValidation]); + const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagNameValidation, attributesValidation, propertiesValidation]); if (error.length) { return res .status(400) @@ -38,7 +38,7 @@ const treatmentValidation = (req, res, next) => { matchingKey: matchingKeyValidation.value, featureFlagName: featureFlagNameValidation.value, attributes: attributesValidation.value, - options: optionsValidation.value, + properties: propertiesValidation.value, }; if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; @@ -58,9 +58,9 @@ const treatmentsValidation = (req, res, next) => { const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; const featureFlagsNameValidation = splitsValidator(req.query['split-names']); const attributesValidation = attributesValidator(req.query.attributes); - const optionsValidation = validateEvaluationOptions(req.query.options); + const propertiesValidation = validateProperties(req.query.properties, req.method === 'GET'); - const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagsNameValidation, attributesValidation, optionsValidation]); + const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagsNameValidation, attributesValidation, propertiesValidation]); if (error.length) { return res .status(400) @@ -72,7 +72,7 @@ const treatmentsValidation = (req, res, next) => { matchingKey: matchingKeyValidation.value, featureFlagNames: featureFlagsNameValidation.value, attributes: attributesValidation.value, - options: optionsValidation.value, + properties: propertiesValidation.value, }; if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; @@ -92,9 +92,9 @@ const flagSetsValidation = (req, res, next) => { const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; const flagSetNameValidation = flagSetsValidator(req.query['flag-sets']); const attributesValidation = attributesValidator(req.query.attributes); - const optionsValidation = validateEvaluationOptions(req.query.options); + const propertiesValidation = validateProperties(req.query.properties, req.method === 'GET'); - const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, flagSetNameValidation, attributesValidation, optionsValidation]); + const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, flagSetNameValidation, attributesValidation, propertiesValidation]); if (error.length) { return res .status(400) @@ -106,7 +106,7 @@ const flagSetsValidation = (req, res, next) => { matchingKey: matchingKeyValidation.value, flagSetNames: flagSetNameValidation.value, attributes: attributesValidation.value, - options: optionsValidation.value, + properties: propertiesValidation.value, }; if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; @@ -126,7 +126,7 @@ const trackValidation = (req, res, next) => { const trafficTypeValidation = trafficTypeValidator(req.query['traffic-type']); const eventTypeValidation = eventTypeValidator(req.query['event-type']); const valueValidation = valueValidator(req.query.value); - const propertiesValidation = validateProperties(req.query.properties); + const propertiesValidation = validateProperties(req.query.properties, req.method === 'GET'); const error = parseValidators([keyValidation, trafficTypeValidation, eventTypeValidation, valueValidation, propertiesValidation]); if (error.length) { @@ -157,9 +157,9 @@ const trackValidation = (req, res, next) => { const allTreatmentValidation = (req, res, next) => { const keysValidation = keysValidator(req.query.keys); const attributesValidation = attributesValidator(req.query.attributes); - const optionsValidation = validateEvaluationOptions(req.query.options); + const propertiesValidation = validateProperties(req.query.properties, req.method === 'GET'); - const error = parseValidators([keysValidation, attributesValidation, optionsValidation]); + const error = parseValidators([keysValidation, attributesValidation, propertiesValidation]); if (error.length) { return res .status(400) @@ -170,7 +170,7 @@ const allTreatmentValidation = (req, res, next) => { req.splitio = { keys: keysValidation.value, attributes: attributesValidation.value, - options: optionsValidation.value, + properties: propertiesValidation.value, }; } @@ -185,9 +185,8 @@ const fwdAttributesFromPost = function parseAttributesMiddleware(req, res, next) next(); }; -const fwdEvaluationOptionsFromPost = function parseOptionsMiddleware(req, res, next) { - req.query.options = req.body.options; - +const fwdPropertiesFromPost = function parsePropertiesMiddleware(req, res, next) { + req.query.properties = req.body.properties; next(); }; @@ -215,14 +214,14 @@ router.get('/get-all-treatments-with-config', allTreatmentValidation, clientCont // Getting treatments as POST's for big attribute sets const JSON_PARSE_OPTS = { limit: '300kb' }; -router.post('/get-treatment',express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatment); -router.post('/get-treatment-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatmentWithConfig); -router.post('/get-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatments); -router.post('/get-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatmentsWithConfig); -router.post('/get-treatments-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsByFlagSets); -router.post('/get-treatments-with-config-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); -router.post('/get-all-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatments); -router.post('/get-all-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdEvaluationOptionsFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatmentsWithConfig); +router.post('/get-treatment',express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatment); +router.post('/get-treatment-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatmentWithConfig); +router.post('/get-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatments); +router.post('/get-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatmentsWithConfig); +router.post('/get-treatments-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsByFlagSets); +router.post('/get-treatments-with-config-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); +router.post('/get-all-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatments); +router.post('/get-all-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, fwdPropertiesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatmentsWithConfig); // Other methods router.get('/track', trackValidation, clientController.track); diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6d0b6d1..8f2455e 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -61,13 +61,13 @@ components: type: string description: A JSON string of the attributes to include in the evaluation. example: "{\"my-attr1\": \"test\"}" - options: + evaluation-properties: in: query - name: options + name: properties schema: type: string - description: A JSON string of the options to include in the evaluation. - example: "{\"properties\": {\"package\":\"premium\",\"admin\":true,\"discount\":50}}" + description: A JSON string of the properties to include in the evaluation. Must be a flat object with up to 15 keys for GET requests. Values must be boolean, string, number, or null. + example: '{"package":"premium","admin":true,"discount":50}' keys: in: query name: keys @@ -108,7 +108,7 @@ components: example: "{\"prop1\": 1}" requestBodies: EvaluationRequestBody: - description: A JSON of the attributes and options to include in the evaluation. + description: A JSON of the attributes and properties to include in the evaluation. required: false content: application/json: @@ -120,6 +120,8 @@ components: properties: attributes: type: object + properties: + type: object example: { attributes: { myattr1: 'test' }, properties: {"package":"premium","admin":true,"discount":50}} InputValidation: type: object @@ -219,7 +221,7 @@ paths: - $ref: '#/components/parameters/split-name' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -227,7 +229,7 @@ paths: application/json: schema: type: object - properties: + evaluation-properties: splitName: type: string treatment: @@ -252,7 +254,7 @@ paths: post: tags: - client - summary: performs single evaluation but receiving the attributes and options on the request body. + summary: performs single evaluation but receiving the attributes and properties on the request body. description: Calls getTreatment method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -300,7 +302,7 @@ paths: - $ref: '#/components/parameters/split-name' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -336,7 +338,7 @@ paths: post: tags: - client - summary: performs single evaluation and attachs config, but receiving the attributes and options on the request body. + summary: performs single evaluation and attachs config, but receiving the attributes and properties on the request body. description: Calls getTreatmentWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -387,7 +389,7 @@ paths: - $ref: '#/components/parameters/split-names' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -417,7 +419,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once, but receiving the attributes and options on the request body. + summary: performs multiple evaluation at once, but receiving the attributes and properties on the request body. description: Calls getTreatments method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -462,7 +464,7 @@ paths: - $ref: '#/components/parameters/split-names' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -494,7 +496,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once and attachs config, but receiving the attributes and options on the request body. + summary: performs multiple evaluation at once and attachs config, but receiving the attributes and properties on the request body. description: Calls getTreatmentsWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -541,7 +543,7 @@ paths: - $ref: '#/components/parameters/flag-sets' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -571,7 +573,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once by sets, but receiving the attributes and options on the request body. + summary: performs multiple evaluation at once by sets, but receiving the attributes and properties on the request body. description: Calls getTreatmentsByFlagSets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -616,7 +618,7 @@ paths: - $ref: '#/components/parameters/flag-sets' - $ref: '#/components/parameters/bucketing-key' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -648,7 +650,7 @@ paths: post: tags: - client - summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes and options on the request body. + summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes and properties on the request body. description: Calls getTreatmentsWithConfigBySets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -693,7 +695,7 @@ paths: parameters: - $ref: '#/components/parameters/keys' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -736,7 +738,7 @@ paths: post: tags: - client - summary: matches feature flags for passed trafficType and evaluates with passed key, but receiving the attributes and options on the request body. + summary: matches feature flags for passed trafficType and evaluates with passed key, but receiving the attributes and properties on the request body. description: Calls getTreatments method from the SDK once it have all the splitNames for the keys provided. parameters: - $ref: '#/components/parameters/keys' @@ -790,7 +792,7 @@ paths: parameters: - $ref: '#/components/parameters/keys' - $ref: '#/components/parameters/attributes' - - $ref: '#/components/parameters/options' + - $ref: '#/components/parameters/evaluation-properties' responses: '200': description: Evaluation result @@ -837,7 +839,7 @@ paths: post: tags: - client - summary: matches feature flags for passed trafficType and evaluates with passed key and attaches configs, but receiving the attributes and options on the request body. + summary: matches feature flags for passed trafficType and evaluates with passed key and attaches configs, but receiving the attributes and properties on the request body. description: Calls getTreatmentsWithConfig method from the SDK once it have all the splitNames for the keys provided. parameters: - $ref: '#/components/parameters/keys' diff --git a/utils/constants.js b/utils/constants.js index 31d0948..34a835e 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -3,8 +3,13 @@ const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; const EMPTY_FLAG_SETS = 'you passed an empty flag-sets, flag-sets must be a non-empty array.'; const NULL_FLAG_SETS = 'you passed a null or undefined flag-sets, flag-sets must be a non-empty array.'; +const PROPERTIES_WARNING = 'Warning: properties must be a plain object with only boolean, string, number or null values. Properties will be ignored.'; +const PROPERTIES_KEY_LIMIT_WARNING = 'Warning: properties object must not have more than 15 keys. Properties will be ignored.'; + module.exports = { TRIMMABLE_SPACES_REGEX, EMPTY_FLAG_SETS, NULL_FLAG_SETS, + PROPERTIES_WARNING, + PROPERTIES_KEY_LIMIT_WARNING, }; \ No newline at end of file diff --git a/utils/inputValidation/__tests__/properties.test.js b/utils/inputValidation/__tests__/properties.test.js index 3086ac0..0a1ec60 100644 --- a/utils/inputValidation/__tests__/properties.test.js +++ b/utils/inputValidation/__tests__/properties.test.js @@ -1,30 +1,45 @@ -const { validateProperties, validateEvaluationOptions } = require('../properties'); +const { validateProperties } = require('../properties'); +const { PROPERTIES_WARNING, PROPERTIES_KEY_LIMIT_WARNING } = require('../../constants'); describe('validateProperties', () => { + let logSpy; + beforeEach(() => { + logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + afterEach(() => { + logSpy.mockRestore(); + }); + + const invalidObj = (() => { + const obj = {}; + for (let i = 0; i < 16; i++) obj['k' + i] = i; + return obj; + })(); + test('should return error on invalid properties', done => { - const expected = 'properties must be a plain object with only boolean, string, number or null values.'; const result = validateProperties('test'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); + expect(result).toHaveProperty('valid', true); + expect(result).toHaveProperty('value', null); + expect(result).not.toHaveProperty('error'); + expect(logSpy).toHaveBeenCalledWith(PROPERTIES_WARNING); done(); }); test('should return error on invalid properties 2', done => { - const expected = 'properties must be a plain object with only boolean, string, number or null values.'; const result = validateProperties('[]'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); + expect(result).toHaveProperty('valid', true ); + expect(result).toHaveProperty('value', null); + expect(result).not.toHaveProperty('error'); + expect(logSpy).toHaveBeenCalledWith(PROPERTIES_WARNING); done(); }); test('should return error on invalid properties 3', done => { - const expected = 'properties must be a plain object with only boolean, string, number or null values.'; const result = validateProperties('true'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); + expect(result).toHaveProperty('valid', true); + expect(result).toHaveProperty('value', null); + expect(result).not.toHaveProperty('error'); + expect(logSpy).toHaveBeenCalledWith(PROPERTIES_WARNING); done(); }); @@ -53,75 +68,21 @@ describe('validateProperties', () => { expect(result).not.toHaveProperty('error'); done(); }); -}); - -describe('validateEvaluationOptions', () => { - test('should return error on invalid options', done => { - const expected = 'options must be a plain object.'; - const result = validateEvaluationOptions('test'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); - done(); - }); - - test('should return error on invalid options 2', done => { - const expected = 'options must be a plain object.'; - const result = validateEvaluationOptions('[]'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); - done(); - }); - test('should return error on invalid options 3', done => { - const expected = 'options must be a plain object.'; - const result = validateEvaluationOptions('true'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); - done(); - }); - - test('should be valid when options is an object', done => { - const result = validateEvaluationOptions('{"foo":1}'); - expect(result).toHaveProperty('valid', true); - expect(result).toHaveProperty('value', { foo: 1 }); - expect(result).not.toHaveProperty('error'); - done(); - }); - - test('should be valid when options is empty object', done => { - const result = validateEvaluationOptions('{}'); - expect(result).toHaveProperty('valid', true); - expect(result).toHaveProperty('value', {}); - expect(result).not.toHaveProperty('error'); - done(); - }); - - test('should be valid when options is null', done => { - const result = validateEvaluationOptions(); + test('should not return error if properties has more than 15 keys (enforced)', done => { + const result = validateProperties(invalidObj, true); expect(result).toHaveProperty('valid', true); expect(result).toHaveProperty('value', null); expect(result).not.toHaveProperty('error'); + expect(logSpy).toHaveBeenCalledWith(PROPERTIES_KEY_LIMIT_WARNING); done(); }); - test('should return error if options.properties is not valid', done => { - const expected = 'properties must be a plain object with only boolean, string, number or null values.'; - const result = validateEvaluationOptions('{"properties": "not-an-object"}'); - expect(result).toHaveProperty('valid', false); - expect(result).toHaveProperty('error', expected); - expect(result).not.toHaveProperty('value'); - done(); - }); - - test('should be valid if options.properties is valid', done => { - const result = validateEvaluationOptions('{"properties": {"foo": 1, "bar": "baz", "baz": true}}'); + test('should be valid if properties has more than 15 keys (not enforced)', done => { + const result = validateProperties(invalidObj, false); expect(result).toHaveProperty('valid', true); - expect(result).toHaveProperty('value', { foo: 1, bar: 'baz', baz: true }); + expect(result).toHaveProperty('value', invalidObj); expect(result).not.toHaveProperty('error'); done(); }); - }); diff --git a/utils/inputValidation/properties.js b/utils/inputValidation/properties.js index a082386..32a5fcd 100644 --- a/utils/inputValidation/properties.js +++ b/utils/inputValidation/properties.js @@ -1,8 +1,8 @@ const { isObject, isString } = require('../lang'); -const errorWrapper = require('./wrapper/error'); const okWrapper = require('./wrapper/ok'); +const { PROPERTIES_WARNING, PROPERTIES_KEY_LIMIT_WARNING } = require('../constants'); -function validateProperties(maybeProperties) { +function validateProperties(maybeProperties, enforceKeyLimit = false) { // @TODO make the validation more specific // eslint-disable-next-line eqeqeq if (maybeProperties == undefined) return okWrapper(null); @@ -10,33 +10,21 @@ function validateProperties(maybeProperties) { try { properties = isString(maybeProperties) ? JSON.parse(maybeProperties) : maybeProperties; if (!isObject(properties)) { - return errorWrapper('properties must be a plain object with only boolean, string, number or null values.'); + console.log(PROPERTIES_WARNING); + properties = null; } - return okWrapper(properties); - } catch (e) { - return errorWrapper('properties must be a plain object with only boolean, string, number or null values.'); - } -} - -function validateEvaluationOptions(maybeOptions) { - // eslint-disable-next-line eqeqeq - if (maybeOptions == undefined) return okWrapper(null); - let options; - try { - options = isString(maybeOptions) ? JSON.parse(maybeOptions) : maybeOptions; - if (!isObject(options)) { - return errorWrapper('options must be a plain object.'); + const keys = Object.keys(properties); + if (enforceKeyLimit && keys.length > 15) { + console.log(PROPERTIES_KEY_LIMIT_WARNING); + properties = null; } - if ('properties' in options) { - return validateProperties(options.properties); - } - return okWrapper(options); + return okWrapper(properties); } catch (e) { - return errorWrapper('options must be a plain object.'); + console.log(PROPERTIES_WARNING); + return okWrapper(null); } } module.exports = { validateProperties, - validateEvaluationOptions, }; \ No newline at end of file From 47dc1b7a7df7f495a93eb7b6e249a3d2f37a29dd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 22 Jul 2025 15:44:44 -0300 Subject: [PATCH 3/3] Typos --- CHANGES.txt | 2 +- openapi/openapi.yaml | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 81efca3..9279cd6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,7 +10,7 @@ - Updated base image to node:24.3.0-alpine3.21 2.7.1 (Jun 25, 2025) - - Fixed OpenAPI spec fot /manager/splits + - Fixed OpenAPI spec for /manager/splits - Updated base image to node:24.2.0-alpine3.22 2.7.0 (Dec 20, 2024) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 8f2455e..fbaa1fe 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -67,7 +67,7 @@ components: schema: type: string description: A JSON string of the properties to include in the evaluation. Must be a flat object with up to 15 keys for GET requests. Values must be boolean, string, number, or null. - example: '{"package":"premium","admin":true,"discount":50}' + example: "{\"package\": \"premium\", \"admin\": true, \"discount\": 50 }" keys: in: query name: keys @@ -122,7 +122,7 @@ components: type: object properties: type: object - example: { attributes: { myattr1: 'test' }, properties: {"package":"premium","admin":true,"discount":50}} + example: { attributes: { myattr1: "test" }, properties: { package: "premium", admin: true, discount: 50 }} InputValidation: type: object properties: @@ -295,7 +295,7 @@ paths: get: tags: - client - summary: performs single evaluation and attachs config + summary: performs single evaluation and attaches config description: Calls getTreatmentWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -338,7 +338,7 @@ paths: post: tags: - client - summary: performs single evaluation and attachs config, but receiving the attributes and properties on the request body. + summary: performs single evaluation and attaches config, but receiving the attributes and properties on the request body. description: Calls getTreatmentWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -457,7 +457,7 @@ paths: get: tags: - client - summary: performs multiple evaluation at once and attachs config + summary: performs multiple evaluation at once and attaches config description: Calls getTreatmentsWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -496,7 +496,7 @@ paths: post: tags: - client - summary: performs multiple evaluation at once and attachs config, but receiving the attributes and properties on the request body. + summary: performs multiple evaluation at once and attaches config, but receiving the attributes and properties on the request body. description: Calls getTreatmentsWithConfig method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -611,7 +611,7 @@ paths: get: tags: - client - summary: performs multiple evaluation by sets at once and attachs config + summary: performs multiple evaluation by sets at once and attaches config description: Calls getTreatmentsWithConfigBySets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -650,7 +650,7 @@ paths: post: tags: - client - summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes and properties on the request body. + summary: performs multiple evaluation by sets at once and attaches config, but receiving the attributes and properties on the request body. description: Calls getTreatmentsWithConfigBySets method from the SDK. parameters: - $ref: '#/components/parameters/key' @@ -1238,4 +1238,3 @@ paths: application/json: schema: $ref: '#/components/schemas/Unauthorized' -