From f245e0a2b1ea40e12cd4250640bbd946ef71741d Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Tue, 25 Jun 2024 18:19:27 +0300 Subject: [PATCH 1/9] feat: Add a class decrator with options to validate any needed prop/method --- packages/grid_client/src/helpers/validator.ts | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 15bb9c4a75..1f93d7f865 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -31,4 +31,106 @@ function validateHexSeed(seed: string, length: number): boolean { return true; } -export { validateObject, validateInput, validateHexSeed }; +interface ValidationOptions { + props?: boolean | string | string[]; + methods?: boolean | string | string[]; +} + +function Validate(options?: ValidationOptions): ClassDecorator { + const _options = _normalizeValidationOptions(options); + return (target: any): any => { + const methods = _getMethods(target, _options); + for (const method of methods) { + const fn = target.prototype[method].bind(target.prototype); + target.prototype[method] = function (...args: any[]): any { + const errors = validateSync(this); + if (errors.length) { + throw errors; + } + return fn(...args); + }; + } + + return class extends target { + constructor(...args: any[]) { + super(args); + + const props = _getProps(this, _options); + for (const prop of props) { + let _value = this[prop]; + + Object.defineProperty(this, prop, { + configurable: false, + enumerable: true, + get: () => _value, + set(value) { + const errors = validateSync(this); + for (const error of errors) { + if (error.property === prop) { + throw error; + } + } + _value = value; + }, + }); + } + } + }; + }; +} + +function _normalizeValidationOptions(options?: ValidationOptions): Required { + return { + props: options?.props ?? true, + methods: options?.methods ?? true, + }; +} + +function _getProps(ctor: any, options: Required): string[] { + /* This env variable should be used while testing to prevent throw error while setting values */ + if (process.env.SKIP_PROPS_VALIDATION) { + return []; + } + + if (options.props === true) { + return Object.getOwnPropertyNames(ctor); + } + + if (typeof options.props === "string") { + return [options.props]; + } + + if (Array.isArray(options.props)) { + return options.props; + } + + return []; +} + +function _getMethods(ctor: any, options: Required): string[] { + /* This env variable should be used to prevent throw error while calling methods if needed */ + if (process.env.SKIP_METHODS_VALIDATION) { + return []; + } + + if (options.methods === true) { + const methods = Object.getOwnPropertyNames(ctor.prototype); + const constructorIndex = methods.indexOf("constructor"); + if (constructorIndex !== -1) { + methods.splice(constructorIndex, 1); + } + return methods; + } + + if (typeof options.methods === "string") { + return [options.methods]; + } + + if (Array.isArray(options.methods)) { + return options.methods; + } + + return []; +} + +export { validateObject, validateInput, validateHexSeed, type ValidationOptions, Validate }; From beda2d170fb435a3ee6b25ec3092f2da09008af4 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Tue, 25 Jun 2024 18:20:00 +0300 Subject: [PATCH 2/9] feat: Use Validate class decrator in ComputeCapacity class to throw error when needed --- packages/grid_client/src/zos/computecapacity.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/grid_client/src/zos/computecapacity.ts b/packages/grid_client/src/zos/computecapacity.ts index 6b9ac9f33d..480ceee13a 100644 --- a/packages/grid_client/src/zos/computecapacity.ts +++ b/packages/grid_client/src/zos/computecapacity.ts @@ -1,6 +1,9 @@ import { Expose } from "class-transformer"; import { IsInt, Max, Min } from "class-validator"; +import { Validate } from "../helpers/validator"; + +@Validate() class ComputeCapacity { @Expose() @IsInt() @Min(1) @Max(32) cpu: number; @Expose() @IsInt() @Min(256 * 1024 ** 2) @Max(256 * 1024 ** 3) memory: number; // in bytes From 02191e465337d2f9d618a62a3c6a65bb2a475e60 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Tue, 25 Jun 2024 18:20:34 +0300 Subject: [PATCH 3/9] chore: Remove skip from tests and run them using SKIP_PROPS_VALIDATION=true env var --- .../tests/modules/compute_capacity.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grid_client/tests/modules/compute_capacity.test.ts b/packages/grid_client/tests/modules/compute_capacity.test.ts index 81501d6908..c9568f23bf 100644 --- a/packages/grid_client/tests/modules/compute_capacity.test.ts +++ b/packages/grid_client/tests/modules/compute_capacity.test.ts @@ -6,12 +6,12 @@ beforeEach(() => { computeCapacity = new ComputeCapacity(); }); describe("Compute Capacity module", () => { - test.skip("Compute Capacity instance is of type ComputeCapacity.", () => { + test("Compute Capacity instance is of type ComputeCapacity.", () => { expect(computeCapacity).toBeInstanceOf(ComputeCapacity); }); // The following tests are skipped as there's an issue w input validation. Should be returned once validation is fixed here: https://github.com/threefoldtech/tfgrid-sdk-ts/issues/2821 - test.skip("Min values for cpu & memory.", () => { + test("Min values for cpu & memory.", () => { const cpu = 0; const mem = 255 * 1024 ** 2; @@ -23,7 +23,7 @@ describe("Compute Capacity module", () => { expect(result).toThrow(); }); - test.skip("Max values for cpu & memory.", () => { + test("Max values for cpu & memory.", () => { const cpu = 33; const mem = 255 * 1024 ** 4; @@ -35,7 +35,7 @@ describe("Compute Capacity module", () => { expect(result).toThrow(); }); - test.skip("cpu & memory doesn't accept decimal values.", () => { + test("cpu & memory doesn't accept decimal values.", () => { const cpu = 1.5; const mem = 1.2; @@ -47,13 +47,13 @@ describe("Compute Capacity module", () => { expect(result).toThrow(); }); - test.skip("cpu & memory empty values.", () => { + test("cpu & memory empty values.", () => { const result = () => computeCapacity.challenge(); expect(result).toThrow(); }); - test.skip("An error should be thrown if cpu & memory negative values.", () => { + test("An error should be thrown if cpu & memory negative values.", () => { const negative_cpu = -1; const negative_mem = -1; From e18e0b4333e741ac7fa73a4b7ef4186f3b9a9ddf Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Tue, 25 Jun 2024 18:33:41 +0300 Subject: [PATCH 4/9] fix: Bind to 'this'(aka. current instance) instead of ctor.prototype --- packages/grid_client/src/helpers/validator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 1f93d7f865..713845a8f2 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -41,13 +41,13 @@ function Validate(options?: ValidationOptions): ClassDecorator { return (target: any): any => { const methods = _getMethods(target, _options); for (const method of methods) { - const fn = target.prototype[method].bind(target.prototype); + const fn = target.prototype[method]; target.prototype[method] = function (...args: any[]): any { const errors = validateSync(this); if (errors.length) { throw errors; } - return fn(...args); + return fn.apply(this, args); }; } From 239de122d63e96f4dd407688e54341efcdc7efe4 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Tue, 25 Jun 2024 18:36:59 +0300 Subject: [PATCH 5/9] fix: split args in super call --- packages/grid_client/src/helpers/validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 713845a8f2..86311118ac 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -53,7 +53,7 @@ function Validate(options?: ValidationOptions): ClassDecorator { return class extends target { constructor(...args: any[]) { - super(args); + super(...args); const props = _getProps(this, _options); for (const prop of props) { From f031ab196b0276784132e4d355a54bbd22ea7501 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 26 Jun 2024 10:33:46 +0300 Subject: [PATCH 6/9] Update grid client workflow to include skip props env --- .github/workflows/grid_client_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/grid_client_tests.yml b/.github/workflows/grid_client_tests.yml index 54521bb612..a7149112f5 100644 --- a/.github/workflows/grid_client_tests.yml +++ b/.github/workflows/grid_client_tests.yml @@ -23,6 +23,7 @@ jobs: RMB_PROXY: true STORE_SECRET: secret MNEMONIC: ${{ secrets.MNEMONIC }} + SKIP_PROPS_VALIDATION: true steps: - name: set network for Schedule if: ${{env.NETWORK}} == "" From 16b9cda30bbaa302bc91fcb610623572a44f1263 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Wed, 26 Jun 2024 13:27:05 +0300 Subject: [PATCH 7/9] - chore: Rename decrator to ValidateMembers instead of Validate - fix: Assign prop value before validate --- packages/grid_client/src/helpers/validator.ts | 6 +++--- packages/grid_client/src/zos/computecapacity.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 86311118ac..163b160737 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -36,7 +36,7 @@ interface ValidationOptions { methods?: boolean | string | string[]; } -function Validate(options?: ValidationOptions): ClassDecorator { +function ValidateMembers(options?: ValidationOptions): ClassDecorator { const _options = _normalizeValidationOptions(options); return (target: any): any => { const methods = _getMethods(target, _options); @@ -64,13 +64,13 @@ function Validate(options?: ValidationOptions): ClassDecorator { enumerable: true, get: () => _value, set(value) { + _value = value; const errors = validateSync(this); for (const error of errors) { if (error.property === prop) { throw error; } } - _value = value; }, }); } @@ -133,4 +133,4 @@ function _getMethods(ctor: any, options: Required): string[] return []; } -export { validateObject, validateInput, validateHexSeed, type ValidationOptions, Validate }; +export { validateObject, validateInput, validateHexSeed, type ValidationOptions, ValidateMembers }; diff --git a/packages/grid_client/src/zos/computecapacity.ts b/packages/grid_client/src/zos/computecapacity.ts index 480ceee13a..d4939a3e2f 100644 --- a/packages/grid_client/src/zos/computecapacity.ts +++ b/packages/grid_client/src/zos/computecapacity.ts @@ -1,9 +1,9 @@ import { Expose } from "class-transformer"; import { IsInt, Max, Min } from "class-validator"; -import { Validate } from "../helpers/validator"; +import { ValidateMembers } from "../helpers/validator"; -@Validate() +@ValidateMembers() class ComputeCapacity { @Expose() @IsInt() @Min(1) @Max(32) cpu: number; @Expose() @IsInt() @Min(256 * 1024 ** 2) @Max(256 * 1024 ** 3) memory: number; // in bytes From 497b58697d615b895187e652256564ab7a555756 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Wed, 26 Jun 2024 13:50:37 +0300 Subject: [PATCH 8/9] chore: Add some jsdocs for validatemembers decrator --- packages/grid_client/src/helpers/validator.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 163b160737..2d6c5cb809 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -36,6 +36,51 @@ interface ValidationOptions { methods?: boolean | string | string[]; } +/* + * */ + +/** + * @description + * This `ValidateMembers` is a config method which returns back a *classDecrator* + * Allows to configure which setter/methods should trigger validation for that specific class + * + * Example As follow + * @example + * ```typescript + * import { isLength, isInt } from "class-validator"; + * + * // ⁣@ValidateMembers({ props: false, methods: true }) // - disable validation on set props + * // ⁣@ValidateMembers({ props: true, methods: false }) // - disable validation on call methods + * // ⁣@ValidateMembers({ props: 'name', methods: false }) // - validate only on setting 'name' prop + * // And so on... + * ⁣@ValidateMembers() // = ⁣@ValidateMembers({ props: true, methods: true }) + * class User { + * ⁣@isLength(2) name: string + * ⁣⁣@isInt() age: number + * + * greeting() { + * // Some logic + * } + * } + * ``` + * + * + * + * @param options { ValidationOptions | undefined } + * @returns { ClassDecorator } + */ function ValidateMembers(options?: ValidationOptions): ClassDecorator { const _options = _normalizeValidationOptions(options); return (target: any): any => { From 40e824da21ac48a23bc082ba98afa2cbc81a632a Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Mon, 1 Jul 2024 16:22:43 +0300 Subject: [PATCH 9/9] - chore: Remove env variable while 'SKIP_PROPS_VALIDATION' while test - feat: Add throw expectation while setting cpu/memory in ComputeCapacity --- .github/workflows/grid_client_tests.yml | 1 - .../tests/modules/compute_capacity.test.ts | 28 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/grid_client_tests.yml b/.github/workflows/grid_client_tests.yml index a7149112f5..54521bb612 100644 --- a/.github/workflows/grid_client_tests.yml +++ b/.github/workflows/grid_client_tests.yml @@ -23,7 +23,6 @@ jobs: RMB_PROXY: true STORE_SECRET: secret MNEMONIC: ${{ secrets.MNEMONIC }} - SKIP_PROPS_VALIDATION: true steps: - name: set network for Schedule if: ${{env.NETWORK}} == "" diff --git a/packages/grid_client/tests/modules/compute_capacity.test.ts b/packages/grid_client/tests/modules/compute_capacity.test.ts index c9568f23bf..197bfcd074 100644 --- a/packages/grid_client/tests/modules/compute_capacity.test.ts +++ b/packages/grid_client/tests/modules/compute_capacity.test.ts @@ -15,11 +15,12 @@ describe("Compute Capacity module", () => { const cpu = 0; const mem = 255 * 1024 ** 2; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); @@ -27,11 +28,12 @@ describe("Compute Capacity module", () => { const cpu = 33; const mem = 255 * 1024 ** 4; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); @@ -39,11 +41,12 @@ describe("Compute Capacity module", () => { const cpu = 1.5; const mem = 1.2; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); @@ -57,11 +60,12 @@ describe("Compute Capacity module", () => { const negative_cpu = -1; const negative_mem = -1; - computeCapacity.cpu = negative_cpu; - computeCapacity.memory = negative_mem; - + const setCPU = () => (computeCapacity.cpu = negative_cpu); + const setMem = () => (computeCapacity.memory = negative_mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); });