From ff1716177998bc407a2ccd3b0b50d3327530c927 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Wed, 24 Jan 2024 17:20:02 +0000 Subject: [PATCH 1/9] Convert legacy decorators to new style --- src/api/dto/ContributionDto.ts | 9 ++-- src/api/validators/IsPassword.ts | 45 +++++++++------- src/api/validators/IsValidPayFee.ts | 43 +++++++++++++++ src/api/validators/MinContributionAmount.ts | 58 ++++++++++++++------- src/api/validators/ValidPayFee.ts | 35 ------------- 5 files changed, 112 insertions(+), 78 deletions(-) create mode 100644 src/api/validators/IsValidPayFee.ts delete mode 100644 src/api/validators/ValidPayFee.ts diff --git a/src/api/dto/ContributionDto.ts b/src/api/dto/ContributionDto.ts index 5908fe1b6..c282c4e8e 100644 --- a/src/api/dto/ContributionDto.ts +++ b/src/api/dto/ContributionDto.ts @@ -9,24 +9,23 @@ import { IsIn, IsNumber, IsOptional, - IsString, - Validate + IsString } from "class-validator"; import IsUrl from "@api/validators/IsUrl"; +import IsValidPayFee from "@api/validators/IsValidPayFee"; import MinContributionAmount from "@api/validators/MinContributionAmount"; -import ValidPayFee from "@api/validators/ValidPayFee"; import { StartJoinFlowDto } from "./JoinFlowDto"; export class UpdateContributionDto { - @Validate(MinContributionAmount) + @MinContributionAmount() amount!: number; @IsEnum(ContributionPeriod) period!: ContributionPeriod; - @Validate(ValidPayFee) + @IsValidPayFee() payFee!: boolean; @IsBoolean() diff --git a/src/api/validators/IsPassword.ts b/src/api/validators/IsPassword.ts index 288fe6ebe..ba87250ce 100644 --- a/src/api/validators/IsPassword.ts +++ b/src/api/validators/IsPassword.ts @@ -1,22 +1,29 @@ -import { - ValidationArguments, - ValidatorConstraint, - ValidatorConstraintInterface -} from "class-validator"; +import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; -@ValidatorConstraint({ name: "isPassword" }) -export default class IsPassword implements ValidatorConstraintInterface { - validate(password: unknown): boolean | Promise { - return ( - typeof password === "string" && - password.length >= 8 && - /[a-z]/.test(password) && - /[A-Z]/.test(password) && - /[0-9]/.test(password) - ); - } +function isPassword(password: unknown): boolean { + return ( + typeof password === "string" && + password.length >= 8 && + /[a-z]/.test(password) && + /[A-Z]/.test(password) && + /[0-9]/.test(password) + ); +} - defaultMessage(args: ValidationArguments) { - return `${args.property} does not meet password requirements`; - } +export default function IsPassword( + validationOptions?: ValidationOptions +): PropertyDecorator { + return ValidateBy( + { + name: "isPassword", + validator: { + validate: isPassword, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + "$property must be a valid password", + validationOptions + ) + } + }, + validationOptions + ); } diff --git a/src/api/validators/IsValidPayFee.ts b/src/api/validators/IsValidPayFee.ts new file mode 100644 index 000000000..368c7b6cc --- /dev/null +++ b/src/api/validators/IsValidPayFee.ts @@ -0,0 +1,43 @@ +import { ContributionPeriod } from "@beabee/beabee-common"; +import { + ValidateBy, + ValidationArguments, + ValidationOptions, + buildMessage +} from "class-validator"; + +import OptionsService from "@core/services/OptionsService"; + +function isValidPayFee(value: unknown, args?: ValidationArguments): boolean { + if (typeof value !== "boolean" || !args) return false; + + // Show always be false if the option is disabled + if (!OptionsService.getBool("show-absorb-fee")) return value === false; + + const amount = "amount" in args.object && args.object.amount; + const period = "period" in args.object && args.object.period; + // Annual contributions don't pay a fee + if (value && period === ContributionPeriod.Annually) { + return false; + } + // £1 monthly contributions must pay fee + if (!value && period === ContributionPeriod.Monthly && amount === 1) { + return false; + } + return true; +} + +export default function IsValidPayFee( + validationOptions?: ValidationOptions +): PropertyDecorator { + return ValidateBy({ + name: "isValidPayFee", + validator: { + validate: isValidPayFee, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + `$property is not valid`, + validationOptions + ) + } + }); +} diff --git a/src/api/validators/MinContributionAmount.ts b/src/api/validators/MinContributionAmount.ts index d354d8ca1..5a9879625 100644 --- a/src/api/validators/MinContributionAmount.ts +++ b/src/api/validators/MinContributionAmount.ts @@ -1,29 +1,49 @@ import { ContributionPeriod } from "@beabee/beabee-common"; import { + buildMessage, + ValidateBy, ValidationArguments, - ValidatorConstraint, - ValidatorConstraintInterface + ValidationOptions } from "class-validator"; import OptionsService from "@core/services/OptionsService"; -@ValidatorConstraint({ name: "minContributionAmount" }) -export default class MinContributionAmount - implements ValidatorConstraintInterface -{ - validate(amount: unknown, args: ValidationArguments): boolean { - return typeof amount === "number" && amount >= this.minAmount(args); - } +function getMinAmount(args: ValidationArguments | undefined): number | false { + const minMonthlyAmount = OptionsService.getInt( + "contribution-min-monthly-amount" + ); - defaultMessage(args: ValidationArguments) { - return `${args.property} must be at least ${this.minAmount(args)}`; - } + const period = args && "period" in args.object && args.object.period; + return period === ContributionPeriod.Monthly + ? minMonthlyAmount + : period === ContributionPeriod.Annually + ? minMonthlyAmount * 12 + : false; +} - private minAmount(args: ValidationArguments) { - const period = (args.object as any)?.period as unknown; - return ( - OptionsService.getInt("contribution-min-monthly-amount") * - (period === ContributionPeriod.Annually ? 12 : 1) - ); - } +export default function MinContributionAmount( + validationOptions?: ValidationOptions +): PropertyDecorator { + return ValidateBy( + { + name: "minContributionAmount", + validator: { + validate: (value, args) => { + const minAmount = getMinAmount(args); + return ( + minAmount !== false && + typeof value === "number" && + value > minAmount + ); + }, + defaultMessage: buildMessage((eachPrefix, args) => { + const minAmount = !!args && getMinAmount(args); + return minAmount === false + ? eachPrefix + `must have a valid period` + : eachPrefix + `$property must be at least ${minAmount}`; + }, validationOptions) + } + }, + validationOptions + ); } diff --git a/src/api/validators/ValidPayFee.ts b/src/api/validators/ValidPayFee.ts deleted file mode 100644 index e88deb4a5..000000000 --- a/src/api/validators/ValidPayFee.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ContributionPeriod } from "@beabee/beabee-common"; -import { - ValidationArguments, - ValidatorConstraint, - ValidatorConstraintInterface -} from "class-validator"; - -import OptionsService from "@core/services/OptionsService"; - -@ValidatorConstraint({ name: "validPayFee" }) -export default class ValidPayFee implements ValidatorConstraintInterface { - validate(payFee: unknown, args: ValidationArguments): boolean { - return typeof payFee === "boolean" && this.validPayFee(payFee, args); - } - - defaultMessage(args: ValidationArguments) { - return `${args.property} is not valid`; - } - - private validPayFee(payFee: boolean, args: ValidationArguments): boolean { - if (!OptionsService.getBool("show-absorb-fee")) return payFee === false; - - const amount = (args.object as any)?.amount as unknown; - const period = (args.object as any)?.period as unknown; - // Annual contributions don't pay a fee - if (payFee && period === ContributionPeriod.Annually) { - return false; - } - // £1 monthly contributions must pay fee - if (!payFee && period === ContributionPeriod.Monthly && amount === 1) { - return false; - } - return true; - } -} From 17c53c497b4320286b1b7496166b90c0cfacc063 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Thu, 25 Jan 2024 16:45:43 +0000 Subject: [PATCH 2/9] Add tests for validators --- src/api/validators/IsLngLat.test.ts | 28 ++++++++++ src/api/validators/IsMapBounds.test.ts | 43 ++++++++++++++++ src/api/validators/IsMapBounds.ts | 12 +++-- src/api/validators/IsPassword.test.ts | 30 +++++++++++ src/api/validators/IsPassword.ts | 2 +- src/api/validators/IsSlug.test.ts | 26 ++++++++++ src/api/validators/IsSlug.ts | 8 +-- src/api/validators/IsType.test.ts | 23 +++++++++ src/api/validators/IsType.ts | 20 ++++++-- src/api/validators/IsValidPayFee.test.ts | 29 +++++++++++ src/api/validators/IsValidPayFee.ts | 37 ++++++++++---- .../validators/MinContributionAmount.test.ts | 32 ++++++++++++ src/api/validators/MinContributionAmount.ts | 51 ++++++++++++++----- 13 files changed, 306 insertions(+), 35 deletions(-) create mode 100644 src/api/validators/IsLngLat.test.ts create mode 100644 src/api/validators/IsMapBounds.test.ts create mode 100644 src/api/validators/IsPassword.test.ts create mode 100644 src/api/validators/IsSlug.test.ts create mode 100644 src/api/validators/IsType.test.ts create mode 100644 src/api/validators/IsValidPayFee.test.ts create mode 100644 src/api/validators/MinContributionAmount.test.ts diff --git a/src/api/validators/IsLngLat.test.ts b/src/api/validators/IsLngLat.test.ts new file mode 100644 index 000000000..80243aae4 --- /dev/null +++ b/src/api/validators/IsLngLat.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "@jest/globals"; +import { isLngLat } from "./IsLngLat"; + +describe("isLngLat should return true", () => { + it("when given a valid lng/lat pair", () => { + expect(isLngLat([0, 0])).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given a non-array", () => { + expect(isLngLat("foo")).toBe(false); + }); + + it("when given an array with length other than 2", () => { + expect(isLngLat([0])).toBe(false); + expect(isLngLat([0, 0, 0])).toBe(false); + }); + + it("when given an array with non-numbers", () => { + expect(isLngLat(["foo", "bar"])).toBe(false); + }); + + it("when given an array with numbers outside of [-180, 180]", () => { + expect(isLngLat([-181, 0])).toBe(false); + expect(isLngLat([0, 181])).toBe(false); + }); +}); diff --git a/src/api/validators/IsMapBounds.test.ts b/src/api/validators/IsMapBounds.test.ts new file mode 100644 index 000000000..80f146fb5 --- /dev/null +++ b/src/api/validators/IsMapBounds.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "@jest/globals"; +import { isMapBounds } from "./IsMapBounds"; + +describe("isMapBounds should return true", () => { + it("when given a valid bounds", () => { + expect( + isMapBounds([ + [0, 0], + [0, 0] + ]) + ).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given a non-array", () => { + expect(isMapBounds("foo")).toBe(false); + }); + + it("when given an array with length other than 2", () => { + expect(isMapBounds([0])).toBe(false); + expect(isMapBounds([0, 0, 0])).toBe(false); + }); + + it("when given an array with non-LngLat", () => { + expect(isMapBounds(["foo", "bar"])).toBe(false); + }); + + it("when given an array with LngLat outside of [-180, 180]", () => { + expect( + isMapBounds([ + [-181, 0], + [0, 0] + ]) + ).toBe(false); + expect( + isMapBounds([ + [0, 0], + [0, 181] + ]) + ).toBe(false); + }); +}); diff --git a/src/api/validators/IsMapBounds.ts b/src/api/validators/IsMapBounds.ts index dabf33955..2d52d135e 100644 --- a/src/api/validators/IsMapBounds.ts +++ b/src/api/validators/IsMapBounds.ts @@ -1,6 +1,12 @@ import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; import { isLngLat } from "./IsLngLat"; +export function isMapBounds( + value: unknown +): value is [[number, number], [number, number]] { + return Array.isArray(value) && value.length === 2 && value.every(isLngLat); +} + export default function IsMapBounds( validationOptions?: ValidationOptions ): PropertyDecorator { @@ -8,11 +14,7 @@ export default function IsMapBounds( { name: "isMapBounds", validator: { - validate(value) { - return ( - Array.isArray(value) && value.length === 2 && value.every(isLngLat) - ); - }, + validate: isMapBounds, defaultMessage: buildMessage( (eachPrefix) => eachPrefix + "$property must be a [[lng, lat], [lng, lat]]", diff --git a/src/api/validators/IsPassword.test.ts b/src/api/validators/IsPassword.test.ts new file mode 100644 index 000000000..d25a77505 --- /dev/null +++ b/src/api/validators/IsPassword.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "@jest/globals"; +import { isPassword } from "./IsPassword"; + +describe("isPassword should return true", () => { + it("when given a valid password", () => { + expect(isPassword("Password1")).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given a non-string", () => { + expect(isPassword(123)).toBe(false); + }); + + it("when given a string with length less than 8", () => { + expect(isPassword("Passwor")).toBe(false); + }); + + it("when given a string without lowercase letters", () => { + expect(isPassword("PASSWORD1")).toBe(false); + }); + + it("when given a string without uppercase letters", () => { + expect(isPassword("password1")).toBe(false); + }); + + it("when given a string without numbers", () => { + expect(isPassword("Password")).toBe(false); + }); +}); diff --git a/src/api/validators/IsPassword.ts b/src/api/validators/IsPassword.ts index ba87250ce..641ecab6f 100644 --- a/src/api/validators/IsPassword.ts +++ b/src/api/validators/IsPassword.ts @@ -1,6 +1,6 @@ import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; -function isPassword(password: unknown): boolean { +export function isPassword(password: unknown): boolean { return ( typeof password === "string" && password.length >= 8 && diff --git a/src/api/validators/IsSlug.test.ts b/src/api/validators/IsSlug.test.ts new file mode 100644 index 000000000..04e31494c --- /dev/null +++ b/src/api/validators/IsSlug.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "@jest/globals"; +import { isSlug } from "./IsSlug"; + +describe("isSlug should return true", () => { + it("when given a valid slug", () => { + expect(isSlug("foo-bar")).toBe(true); + }); + + it("when given a string with uppercase letters", () => { + expect(isSlug("FooBar")).toBe(true); + }); + + it("when given a string with valid non-alphanumeric characters", () => { + expect(isSlug("foo_bar")).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given a non-string", () => { + expect(isSlug(123)).toBe(false); + }); + + it("when given a string with forbidden characters", () => { + expect(isSlug("foo bar ?? &")).toBe(false); + }); +}); diff --git a/src/api/validators/IsSlug.ts b/src/api/validators/IsSlug.ts index d1c9179b5..6ae18c773 100644 --- a/src/api/validators/IsSlug.ts +++ b/src/api/validators/IsSlug.ts @@ -1,6 +1,10 @@ import { buildMessage, ValidateBy, ValidationOptions } from "class-validator"; import slugify from "slugify"; +export function isSlug(slug: unknown): boolean { + return typeof slug === "string" && slug === slugify(slug); +} + export default function IsSlug( validationOptions?: ValidationOptions ): PropertyDecorator { @@ -8,9 +12,7 @@ export default function IsSlug( { name: "isSlug", validator: { - validate(value) { - return typeof value === "string" && value === slugify(value); - }, + validate: isSlug, defaultMessage: buildMessage( (eachPrefix) => eachPrefix + "$property must be a slug", validationOptions diff --git a/src/api/validators/IsType.test.ts b/src/api/validators/IsType.test.ts new file mode 100644 index 000000000..69ab12188 --- /dev/null +++ b/src/api/validators/IsType.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "@jest/globals"; +import { isType } from "./IsType"; + +describe("isType should return true", () => { + it("when the value matches the type", () => { + expect(isType(["string"], "foo")).toBe(true); + }); + + it("when the value matches one of the types", () => { + expect(isType(["string", "number"], "foo")).toBe(true); + expect(isType(["string", "number"], 5)).toBe(true); + }); +}); + +describe("should return false", () => { + it("when the value does not match the type", () => { + expect(isType(["string"], 123)).toBe(false); + }); + + it("when the value does not match any of the types", () => { + expect(isType(["string", "number"], true)).toBe(false); + }); +}); diff --git a/src/api/validators/IsType.ts b/src/api/validators/IsType.ts index 7324f1dd7..0a04b4d72 100644 --- a/src/api/validators/IsType.ts +++ b/src/api/validators/IsType.ts @@ -1,7 +1,21 @@ import { ValidateBy, ValidationOptions } from "class-validator"; import { ValidationArguments } from "class-validator/types/validation/ValidationArguments"; -const IS_TYPE = "isType"; +export function isType( + types: Array< + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function" + >, + value: unknown +): boolean { + return types.includes(typeof value); +} export function IsType( types: Array< @@ -18,9 +32,9 @@ export function IsType( ): PropertyDecorator { return ValidateBy( { - name: IS_TYPE, + name: "isType", validator: { - validate: (value: unknown) => types.includes(typeof value), + validate: (value: unknown) => isType(types, value), defaultMessage: ({ value }: ValidationArguments) => `Current type ${typeof value} is not in [${types.join(", ")}]` } diff --git a/src/api/validators/IsValidPayFee.test.ts b/src/api/validators/IsValidPayFee.test.ts new file mode 100644 index 000000000..4eec9f880 --- /dev/null +++ b/src/api/validators/IsValidPayFee.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "@jest/globals"; +import { isValidPayFee } from "./IsValidPayFee"; +import { ContributionPeriod } from "@beabee/beabee-common"; + +describe("isValidPayFee should return true", () => { + it("when given a valid pay fee", () => { + expect(isValidPayFee(true, 5, ContributionPeriod.Monthly)).toBe(true); + }); + it("when given a valid pay fee", () => { + expect(isValidPayFee(false, 20, ContributionPeriod.Monthly)).toBe(true); + }); + it("when given a valid pay fee", () => { + expect(isValidPayFee(false, 50, ContributionPeriod.Annually)).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given invalid arguments non-boolean", () => { + expect(isValidPayFee("foo", "blah", "blah")).toBe(false); + }); + + it("when trying to pay a fee for an annual contribution", () => { + expect(isValidPayFee(true, 50, ContributionPeriod.Annually)).toBe(false); + }); + + it("when trying to not pay a fee for a monthly contribution of £1", () => { + expect(isValidPayFee(false, 1, ContributionPeriod.Monthly)).toBe(false); + }); +}); diff --git a/src/api/validators/IsValidPayFee.ts b/src/api/validators/IsValidPayFee.ts index 368c7b6cc..d96260e86 100644 --- a/src/api/validators/IsValidPayFee.ts +++ b/src/api/validators/IsValidPayFee.ts @@ -1,21 +1,27 @@ import { ContributionPeriod } from "@beabee/beabee-common"; import { ValidateBy, - ValidationArguments, ValidationOptions, - buildMessage + buildMessage, + isEnum } from "class-validator"; import OptionsService from "@core/services/OptionsService"; +import { isNumber } from "lodash"; -function isValidPayFee(value: unknown, args?: ValidationArguments): boolean { - if (typeof value !== "boolean" || !args) return false; - - // Show always be false if the option is disabled - if (!OptionsService.getBool("show-absorb-fee")) return value === false; +export function isValidPayFee( + value: unknown, + amount: unknown, + period: unknown +): boolean { + if ( + typeof value !== "boolean" || + !isEnum(period, ContributionPeriod) || + !isNumber(amount) + ) { + return false; + } - const amount = "amount" in args.object && args.object.amount; - const period = "period" in args.object && args.object.period; // Annual contributions don't pay a fee if (value && period === ContributionPeriod.Annually) { return false; @@ -24,6 +30,7 @@ function isValidPayFee(value: unknown, args?: ValidationArguments): boolean { if (!value && period === ContributionPeriod.Monthly && amount === 1) { return false; } + return true; } @@ -33,7 +40,17 @@ export default function IsValidPayFee( return ValidateBy({ name: "isValidPayFee", validator: { - validate: isValidPayFee, + validate: (value, args) => { + if (!args) return false; + + // Show always be false if the option is disabled + if (!OptionsService.getBool("show-absorb-fee")) return value === false; + + const amount = "amount" in args.object && args.object.amount; + const period = "period" in args.object && args.object.period; + + return isValidPayFee(value, amount, period as ContributionPeriod); + }, defaultMessage: buildMessage( (eachPrefix) => eachPrefix + `$property is not valid`, validationOptions diff --git a/src/api/validators/MinContributionAmount.test.ts b/src/api/validators/MinContributionAmount.test.ts new file mode 100644 index 000000000..cd062da28 --- /dev/null +++ b/src/api/validators/MinContributionAmount.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "@jest/globals"; +import { minContributionAmount } from "./MinContributionAmount"; + +describe("minContributionAmount should return true", () => { + it("when given a valid contribution amount for monthly", () => { + expect(minContributionAmount(5, "monthly", 5)).toBe(true); + expect(minContributionAmount(7, "monthly", 5)).toBe(true); + }); + + it("when given a valid contribution amount for annual", () => { + expect(minContributionAmount(60, "annually", 5)).toBe(true); + expect(minContributionAmount(70, "annually", 5)).toBe(true); + }); +}); + +describe("should return false", () => { + it("when given invalid arguments non-number", () => { + expect(minContributionAmount("foo", "monthly", 5)).toBe(false); + }); + + it("when given invalid arguments non-enum", () => { + expect(minContributionAmount(5, "foo", 5)).toBe(false); + }); + + it("when given a contribution amount less than the minimum monthly amount", () => { + expect(minContributionAmount(1, "monthly", 2)).toBe(false); + }); + + it("when given a contribution amount less than the minimum annual amount", () => { + expect(minContributionAmount(11, "annually", 1)).toBe(false); + }); +}); diff --git a/src/api/validators/MinContributionAmount.ts b/src/api/validators/MinContributionAmount.ts index 5a9879625..a5ba9ffe7 100644 --- a/src/api/validators/MinContributionAmount.ts +++ b/src/api/validators/MinContributionAmount.ts @@ -1,6 +1,8 @@ import { ContributionPeriod } from "@beabee/beabee-common"; import { buildMessage, + isEnum, + isNumber, ValidateBy, ValidationArguments, ValidationOptions @@ -8,17 +10,41 @@ import { import OptionsService from "@core/services/OptionsService"; -function getMinAmount(args: ValidationArguments | undefined): number | false { +function getMinAmount(period: ContributionPeriod) { const minMonthlyAmount = OptionsService.getInt( "contribution-min-monthly-amount" ); - const period = args && "period" in args.object && args.object.period; return period === ContributionPeriod.Monthly ? minMonthlyAmount - : period === ContributionPeriod.Annually - ? minMonthlyAmount * 12 - : false; + : minMonthlyAmount * 12; +} + +function isPeriod(period: unknown): period is ContributionPeriod { + return isEnum(period, ContributionPeriod); +} + +function getPeriod( + args: ValidationArguments | undefined +): ContributionPeriod | undefined { + return args && + "period" in args.object && + args.object.period && + isPeriod(args.object.period) + ? args.object.period + : undefined; +} + +export function minContributionAmount( + value: unknown, + period: unknown, + minMonthlyAmount: number +): boolean { + if (!isNumber(value) || !isPeriod(period)) return false; + + return period === ContributionPeriod.Monthly + ? value >= minMonthlyAmount + : value >= minMonthlyAmount * 12; } export default function MinContributionAmount( @@ -29,18 +55,17 @@ export default function MinContributionAmount( name: "minContributionAmount", validator: { validate: (value, args) => { - const minAmount = getMinAmount(args); + const period = getPeriod(args); return ( - minAmount !== false && - typeof value === "number" && - value > minAmount + !!period && + minContributionAmount(value, getPeriod(args), getMinAmount(period)) ); }, defaultMessage: buildMessage((eachPrefix, args) => { - const minAmount = !!args && getMinAmount(args); - return minAmount === false - ? eachPrefix + `must have a valid period` - : eachPrefix + `$property must be at least ${minAmount}`; + const period = getPeriod(args); + return period + ? eachPrefix + `$property must be at least ${getMinAmount(period)}` + : eachPrefix + `must have a valid period`; }, validationOptions) } }, From 4308579f26cf9826db98794cdf9b10a588e6e5c3 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Thu, 15 Feb 2024 16:04:28 +0000 Subject: [PATCH 3/9] Use new beabee-common --- package-lock.json | 19 +- package.json | 2 +- src/api/dto/CalloutFormDto.ts | 165 ++++++++---------- src/api/dto/CalloutResponseDto.ts | 8 +- .../CalloutResponseMapTransformer.ts | 4 +- src/apps/polls/app.ts | 6 +- src/core/services/CalloutsService.ts | 6 +- src/models/CalloutResponse.ts | 4 +- src/tools/database/anonymisers/index.ts | 4 +- src/tools/database/anonymisers/models.ts | 5 - src/typings/index.d.ts | 6 +- 11 files changed, 107 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9915de53..32784c19e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.8", "license": "GPL-3.0", "dependencies": { - "@beabee/beabee-common": "^1.19.10", + "@beabee/beabee-common": "^0.20.0-alpha.3", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", "ajv": "^8.12.0", @@ -850,11 +850,20 @@ "dev": true }, "node_modules/@beabee/beabee-common": { - "version": "1.19.10", - "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-1.19.10.tgz", - "integrity": "sha512-v/xZaBFFo9RdS1Bv3PKqZ8j2r3eTrTgUeEGXZDFZPPdXh0Ik8gRx0QDJnpqVs6ESu+7A3HbhuwzBPakiOSQJIw==", + "version": "0.20.0-alpha.3", + "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-0.20.0-alpha.3.tgz", + "integrity": "sha512-bl/Pw+1p+KGRSnwhugicNYzfVzopd+Qyz5hEbLg5g57MxoMGD6fexxZ1AU38ddUPFOIodsm9qXFilaIgr/zF6A==", "dependencies": { - "date-fns": "^2.29.3" + "date-fns": "^3.2.0" + } + }, + "node_modules/@beabee/beabee-common/node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/@colors/colors": { diff --git a/package.json b/package.json index fdd989da0..daf8d1c72 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test": "jest --setupFiles dotenv/config" }, "dependencies": { - "@beabee/beabee-common": "^1.19.10", + "@beabee/beabee-common": "^0.20.0-alpha.3", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", "ajv": "^8.12.0", diff --git a/src/api/dto/CalloutFormDto.ts b/src/api/dto/CalloutFormDto.ts index bb64d331e..fb11cbb2e 100644 --- a/src/api/dto/CalloutFormDto.ts +++ b/src/api/dto/CalloutFormDto.ts @@ -1,12 +1,18 @@ import { - BaseCalloutComponentSchema, CalloutFormSchema, CalloutNavigationSchema, CalloutSlideSchema, - InputCalloutComponentSchema, - NestableCalloutComponentSchema, - RadioCalloutComponentSchema, - SelectCalloutComponentSchema + CalloutComponentBaseSchema, + CalloutComponentBaseInputSchema, + CalloutComponentBaseNestableSchema, + CalloutComponentContentSchema, + CalloutComponentInputSelectSchema, + CalloutComponentType, + calloutComponentNestableTypes, + calloutComponentInputTypes, + CalloutComponentBaseInputSelectableSchema, + calloutComponentInputSelectableTypes, + CalloutComponentSchema } from "@beabee/beabee-common"; import { Transform, Type, plainToInstance } from "class-transformer"; import { @@ -22,35 +28,10 @@ import { validate } from "class-validator"; -// content - -const inputTypes = [ - "address", - "button", - "checkbox", - "currency", - "datetime", - "email", - "file", - "number", - "password", - "phoneNumber", - "signature", - "textfield", - "textarea", - "time", - "url" -] as const; -const nestedTypes = ["panel", "well", "tabs"] as const; -const selectTypes = ["select"] as const; -const radioTypes = ["radio", "selectboxes"] as const; - -abstract class BaseCalloutComponentDto implements BaseCalloutComponentSchema { - abstract type: string; +abstract class CalloutComponentBaseDto implements CalloutComponentBaseSchema { + abstract type: CalloutComponentType; abstract input?: boolean; - [key: string]: unknown; - @IsString() id!: string; @@ -64,28 +45,34 @@ abstract class BaseCalloutComponentDto implements BaseCalloutComponentSchema { @IsOptional() @IsBoolean() adminOnly?: boolean; + + // Unused properties + [key: string]: unknown; } -class ContentCalloutComponentDto extends BaseCalloutComponentDto { +class CalloutComponentContentDto + extends CalloutComponentBaseDto + implements CalloutComponentContentSchema +{ @Equals(false) input!: false; - @IsIn(["content"]) - type!: "content"; + @Equals(CalloutComponentType.CONTENT) + type!: CalloutComponentType.CONTENT; } -class InputCalloutComponentDto - extends BaseCalloutComponentDto - implements InputCalloutComponentSchema +class CalloutComponentInputDto + extends CalloutComponentBaseDto + implements CalloutComponentBaseInputSchema { - @IsIn(inputTypes) - type!: (typeof inputTypes)[number]; + @IsIn(calloutComponentInputTypes) + type!: CalloutComponentBaseInputSchema["type"]; @Equals(true) input!: true; } -class SelectCalloutComponentValueDto { +class CalloutComponentInputSelectDataValueDto { @IsString() label!: string; @@ -93,28 +80,28 @@ class SelectCalloutComponentValueDto { value!: string; } -class SelectCalloutComponentDataDto { +class CalloutComponentInputSelectDataDto { @ValidateNested({ each: true }) - @Type(() => SelectCalloutComponentValueDto) - values!: SelectCalloutComponentValueDto[]; + @Type(() => CalloutComponentInputSelectDataValueDto) + values!: CalloutComponentInputSelectDataValueDto[]; + + // Unused properties + [key: string]: unknown; } -class SelectCalloutComponentDto - extends BaseCalloutComponentDto - implements SelectCalloutComponentSchema +class CalloutComponentInputSelectDto + extends CalloutComponentInputDto + implements CalloutComponentInputSelectSchema { - @IsIn(selectTypes) - type!: (typeof selectTypes)[number]; - - @Equals(true) - input!: true; + @Equals(CalloutComponentType.INPUT_SELECT) + type!: CalloutComponentType.INPUT_SELECT; @ValidateNested() - @Type(() => SelectCalloutComponentDataDto) - data!: SelectCalloutComponentDataDto; + @Type(() => CalloutComponentInputSelectDataDto) + data!: CalloutComponentInputSelectDataDto; } -class RadioCalloutComponentValueDto { +class CalloutComponentInputSelectableValueDto { @IsString() label!: string; @@ -126,19 +113,16 @@ class RadioCalloutComponentValueDto { nextSlideId?: string; } -class RadioCalloutComponentDto - extends BaseCalloutComponentDto - implements RadioCalloutComponentSchema +class CalloutComponentInputSelectableDto + extends CalloutComponentInputDto + implements CalloutComponentBaseInputSelectableSchema { - @IsIn(radioTypes) - type!: (typeof radioTypes)[number]; - - @Equals(true) - input!: true; + @Equals(calloutComponentInputSelectableTypes) + type!: CalloutComponentBaseInputSelectableSchema["type"]; @ValidateNested({ each: true }) - @Type(() => RadioCalloutComponentValueDto) - values!: RadioCalloutComponentValueDto[]; + @Type(() => CalloutComponentInputSelectableValueDto) + values!: CalloutComponentInputSelectableValueDto[]; } function ComponentType() { @@ -149,20 +133,24 @@ function ComponentType() { if (typeof component !== "object" || component === null) throw new Error("Component must be an object"); - switch (true) { - case inputTypes.includes(component.type): - return plainToInstance(InputCalloutComponentDto, component); - case selectTypes.includes(component.type): - return plainToInstance(SelectCalloutComponentDto, component); - case radioTypes.includes(component.type): - return plainToInstance(RadioCalloutComponentDto, component); - case nestedTypes.includes(component.type): - return plainToInstance(NestableCalloutComponentDto, component); - case "content" === component.type: - return plainToInstance(ContentCalloutComponentDto, component); - default: - throw new Error("Unknown component type " + component.type); + switch (component.type) { + case CalloutComponentType.CONTENT: + return plainToInstance(CalloutComponentContentDto, component); + case CalloutComponentType.INPUT_SELECT: + return plainToInstance(CalloutComponentInputSelectDto, component); + } + + if (calloutComponentInputSelectableTypes.includes(component.type)) { + return plainToInstance(CalloutComponentInputSelectableDto, component); } + if (calloutComponentInputTypes.includes(component.type)) { + return plainToInstance(CalloutComponentInputDto, component); + } + if (calloutComponentNestableTypes.includes(component.type)) { + return plainToInstance(CalloutComponentNestableDto, component); + } + + throw new Error("Unknown component type " + component.type); }); }); } @@ -194,26 +182,19 @@ function IsComponent(validationOptions?: ValidationOptions) { ); } -type CalloutComponentDto = - | ContentCalloutComponentDto - | NestableCalloutComponentDto - | InputCalloutComponentDto - | SelectCalloutComponentDto - | RadioCalloutComponentDto; - -class NestableCalloutComponentDto - extends BaseCalloutComponentDto - implements NestableCalloutComponentSchema +class CalloutComponentNestableDto + extends CalloutComponentBaseDto + implements CalloutComponentBaseNestableSchema { - @IsIn(nestedTypes) - type!: (typeof nestedTypes)[number]; + @IsIn(calloutComponentNestableTypes) + type!: CalloutComponentBaseNestableSchema["type"]; @Equals(false) input!: false; @IsComponent({ each: true }) @ComponentType() - components!: CalloutComponentDto[]; + components!: CalloutComponentSchema[]; } class CalloutNavigationDto implements CalloutNavigationSchema { @@ -239,7 +220,7 @@ class CalloutSlideDto implements CalloutSlideSchema { @IsComponent({ each: true }) @ComponentType() - components!: CalloutComponentDto[]; + components!: CalloutComponentSchema[]; @ValidateNested() @Type(() => CalloutNavigationDto) diff --git a/src/api/dto/CalloutResponseDto.ts b/src/api/dto/CalloutResponseDto.ts index 61958b096..7c861323d 100644 --- a/src/api/dto/CalloutResponseDto.ts +++ b/src/api/dto/CalloutResponseDto.ts @@ -2,7 +2,7 @@ import { CalloutComponentSchema, CalloutResponseAnswerAddress, CalloutResponseAnswerFileUpload, - CalloutResponseAnswers, + CalloutResponseAnswersSlide, PaginatedQuery } from "@beabee/beabee-common"; import { Type } from "class-transformer"; @@ -93,7 +93,7 @@ export class GetCalloutResponseDto { @IsOptional() @IsObject() - answers?: CalloutResponseAnswers; + answers?: CalloutResponseAnswersSlide; @IsOptional() @ValidateNested() @@ -119,7 +119,7 @@ export class GetCalloutResponseDto { export class CreateCalloutResponseDto { // TODO: validate @IsObject() - answers!: CalloutResponseAnswers; + answers!: CalloutResponseAnswersSlide; @IsOptional() @IsString() @@ -189,7 +189,7 @@ export class GetCalloutResponseMapDto { number!: number; @IsObject() - answers!: CalloutResponseAnswers; + answers!: CalloutResponseAnswersSlide; @IsString() title!: string; diff --git a/src/api/transformers/CalloutResponseMapTransformer.ts b/src/api/transformers/CalloutResponseMapTransformer.ts index ca2455405..95ca44d68 100644 --- a/src/api/transformers/CalloutResponseMapTransformer.ts +++ b/src/api/transformers/CalloutResponseMapTransformer.ts @@ -2,7 +2,7 @@ import { CalloutResponseAnswer, CalloutResponseAnswerAddress, CalloutResponseAnswerFileUpload, - CalloutResponseAnswers, + CalloutResponseAnswersSlide, getCalloutComponents, stringifyAnswer } from "@beabee/beabee-common"; @@ -42,7 +42,7 @@ class CalloutResponseMapTransformer extends BaseCalloutResponseTransformer< formSchema } = opts.callout; - const answers: CalloutResponseAnswers = Object.fromEntries( + const answers: CalloutResponseAnswersSlide = Object.fromEntries( formSchema.slides.map((slide) => [slide.id, {}]) ); diff --git a/src/apps/polls/app.ts b/src/apps/polls/app.ts index b28210ddd..9c01edbf2 100644 --- a/src/apps/polls/app.ts +++ b/src/apps/polls/app.ts @@ -1,4 +1,4 @@ -import { CalloutResponseAnswers } from "@beabee/beabee-common"; +import { CalloutResponseAnswersSlide } from "@beabee/beabee-common"; import express, { NextFunction, Request, Response } from "express"; import _ from "lodash"; @@ -98,7 +98,7 @@ app.get("/:slug", hasNewModel(Callout, "slug"), (req, res, next) => { async function getUserAnswersAndClear( req: Request -): Promise { +): Promise { const answers = req.session.answers; delete req.session.answers; @@ -183,7 +183,7 @@ app.get( } // Handle partial answers from URL - const answers = req.query.answers as CalloutResponseAnswers; + const answers = req.query.answers as CalloutResponseAnswersSlide; // We don't support allowMultiple callouts at the moment if (!isEmbed && answers && !callout.allowMultiple) { const contact = pollsCode diff --git a/src/core/services/CalloutsService.ts b/src/core/services/CalloutsService.ts index 4236cd894..7e8b9c164 100644 --- a/src/core/services/CalloutsService.ts +++ b/src/core/services/CalloutsService.ts @@ -1,6 +1,6 @@ import { CalloutFormSchema, - CalloutResponseAnswers + CalloutResponseAnswersSlide } from "@beabee/beabee-common"; import { IsNull, LessThan } from "typeorm"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; @@ -102,7 +102,7 @@ class CalloutsService { async setResponse( callout: Callout, contact: Contact, - answers: CalloutResponseAnswers, + answers: CalloutResponseAnswersSlide, isPartial = false ): Promise { if (callout.access === CalloutAccess.OnlyAnonymous) { @@ -158,7 +158,7 @@ class CalloutsService { callout: Callout, guestName: string | undefined, guestEmail: string | undefined, - answers: CalloutResponseAnswers + answers: CalloutResponseAnswersSlide ): Promise { if (callout.access === CalloutAccess.Guest && !(guestName && guestEmail)) { throw new InvalidCalloutResponse("guest-fields-missing"); diff --git a/src/models/CalloutResponse.ts b/src/models/CalloutResponse.ts index 1232ed291..f74137348 100644 --- a/src/models/CalloutResponse.ts +++ b/src/models/CalloutResponse.ts @@ -1,4 +1,4 @@ -import { CalloutResponseAnswers } from "@beabee/beabee-common"; +import { CalloutResponseAnswersSlide } from "@beabee/beabee-common"; import { Column, CreateDateColumn, @@ -41,7 +41,7 @@ export default class CalloutResponse { guestEmail!: string | null; @Column({ type: "jsonb" }) - answers!: CalloutResponseAnswers; + answers!: CalloutResponseAnswersSlide; @Column() isPartial!: boolean; diff --git a/src/tools/database/anonymisers/index.ts b/src/tools/database/anonymisers/index.ts index b8041af24..a5c2c3ff5 100644 --- a/src/tools/database/anonymisers/index.ts +++ b/src/tools/database/anonymisers/index.ts @@ -13,7 +13,7 @@ import Callout from "@models/Callout"; import CalloutResponse from "@models/CalloutResponse"; import { CalloutComponentSchema, - CalloutResponseAnswers + CalloutResponseAnswersSlide } from "@beabee/beabee-common"; import { @@ -79,7 +79,7 @@ function writeItems( function createAnswersMap( components: CalloutComponentSchema[] -): ObjectMap { +): ObjectMap { // return Object.fromEntries( // components.map((c) => [c.key, createComponentAnonymiser(c)]) // ); diff --git a/src/tools/database/anonymisers/models.ts b/src/tools/database/anonymisers/models.ts index 41eba1465..ddd65b106 100644 --- a/src/tools/database/anonymisers/models.ts +++ b/src/tools/database/anonymisers/models.ts @@ -77,15 +77,10 @@ export function createComponentAnonymiser( return chance.pickone([true, false]); case "number": return chance.integer(); - case "password": - return chance.word(); case "textarea": return chance.paragraph(); case "textfield": return chance.sentence(); - case "button": - return v; - case "select": case "radio": case "selectboxes": diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index a67e22923..95d36ac86 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -1,7 +1,7 @@ import { ParamsDictionary } from "express-serve-static-core"; import ApiKey from "@models/ApiKey"; -import { CalloutResponseAnswers } from "@models/CalloutResponse"; +import { CalloutResponseAnswersSlide } from "@models/CalloutResponse"; import Contact from "@models/Contact"; import { AuthInfo as AuthInfo2 } from "@type/auth-info"; @@ -22,7 +22,7 @@ declare global { ): void; model: unknown; allParams: ParamsDictionary; - answers?: CalloutResponseAnswers; + answers?: CalloutResponseAnswersSlide; auth: AuthInfo2 | undefined; } } @@ -36,6 +36,6 @@ declare module "papaparse" { declare module "express-session" { interface SessionData { method?: "plain" | "totp"; - answers: CalloutResponseAnswers | undefined; + answers: CalloutResponseAnswersSlide | undefined; } } From 80e4791e94bdce0afdd6242cf3fe778e82714762 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Thu, 15 Feb 2024 16:08:36 +0000 Subject: [PATCH 4/9] Use @beabee/beabee-common validators --- src/api/validators/IsLngLat.test.ts | 28 -------------- src/api/validators/IsLngLat.ts | 9 +---- src/api/validators/IsMapBounds.test.ts | 43 --------------------- src/api/validators/IsMapBounds.ts | 8 +--- src/api/validators/IsPassword.test.ts | 30 -------------- src/api/validators/IsPassword.ts | 11 +----- src/api/validators/IsSlug.test.ts | 26 ------------- src/api/validators/IsSlug.ts | 6 +-- src/api/validators/IsType.test.ts | 23 ----------- src/api/validators/IsType.ts | 17 +------- src/api/validators/IsValidPayFee.test.ts | 29 -------------- src/api/validators/IsValidPayFee.ts | 35 +---------------- src/api/validators/MinContributionAmount.ts | 7 +--- 13 files changed, 8 insertions(+), 264 deletions(-) delete mode 100644 src/api/validators/IsLngLat.test.ts delete mode 100644 src/api/validators/IsMapBounds.test.ts delete mode 100644 src/api/validators/IsPassword.test.ts delete mode 100644 src/api/validators/IsSlug.test.ts delete mode 100644 src/api/validators/IsType.test.ts delete mode 100644 src/api/validators/IsValidPayFee.test.ts diff --git a/src/api/validators/IsLngLat.test.ts b/src/api/validators/IsLngLat.test.ts deleted file mode 100644 index 80243aae4..000000000 --- a/src/api/validators/IsLngLat.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isLngLat } from "./IsLngLat"; - -describe("isLngLat should return true", () => { - it("when given a valid lng/lat pair", () => { - expect(isLngLat([0, 0])).toBe(true); - }); -}); - -describe("should return false", () => { - it("when given a non-array", () => { - expect(isLngLat("foo")).toBe(false); - }); - - it("when given an array with length other than 2", () => { - expect(isLngLat([0])).toBe(false); - expect(isLngLat([0, 0, 0])).toBe(false); - }); - - it("when given an array with non-numbers", () => { - expect(isLngLat(["foo", "bar"])).toBe(false); - }); - - it("when given an array with numbers outside of [-180, 180]", () => { - expect(isLngLat([-181, 0])).toBe(false); - expect(isLngLat([0, 181])).toBe(false); - }); -}); diff --git a/src/api/validators/IsLngLat.ts b/src/api/validators/IsLngLat.ts index c5b0ffea2..640284d14 100644 --- a/src/api/validators/IsLngLat.ts +++ b/src/api/validators/IsLngLat.ts @@ -1,13 +1,6 @@ +import { isLngLat } from "@beabee/beabee-common"; import { buildMessage, ValidateBy, ValidationOptions } from "class-validator"; -function isAngle(value: unknown): value is number { - return typeof value === "number" && value >= -180 && value <= 180; -} - -export function isLngLat(value: unknown): value is [number, number] { - return Array.isArray(value) && value.length === 2 && value.every(isAngle); -} - export default function IsLngLat( validationOptions?: ValidationOptions ): PropertyDecorator { diff --git a/src/api/validators/IsMapBounds.test.ts b/src/api/validators/IsMapBounds.test.ts deleted file mode 100644 index 80f146fb5..000000000 --- a/src/api/validators/IsMapBounds.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isMapBounds } from "./IsMapBounds"; - -describe("isMapBounds should return true", () => { - it("when given a valid bounds", () => { - expect( - isMapBounds([ - [0, 0], - [0, 0] - ]) - ).toBe(true); - }); -}); - -describe("should return false", () => { - it("when given a non-array", () => { - expect(isMapBounds("foo")).toBe(false); - }); - - it("when given an array with length other than 2", () => { - expect(isMapBounds([0])).toBe(false); - expect(isMapBounds([0, 0, 0])).toBe(false); - }); - - it("when given an array with non-LngLat", () => { - expect(isMapBounds(["foo", "bar"])).toBe(false); - }); - - it("when given an array with LngLat outside of [-180, 180]", () => { - expect( - isMapBounds([ - [-181, 0], - [0, 0] - ]) - ).toBe(false); - expect( - isMapBounds([ - [0, 0], - [0, 181] - ]) - ).toBe(false); - }); -}); diff --git a/src/api/validators/IsMapBounds.ts b/src/api/validators/IsMapBounds.ts index 2d52d135e..e0501c0e8 100644 --- a/src/api/validators/IsMapBounds.ts +++ b/src/api/validators/IsMapBounds.ts @@ -1,11 +1,5 @@ +import { isMapBounds } from "@beabee/beabee-common"; import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; -import { isLngLat } from "./IsLngLat"; - -export function isMapBounds( - value: unknown -): value is [[number, number], [number, number]] { - return Array.isArray(value) && value.length === 2 && value.every(isLngLat); -} export default function IsMapBounds( validationOptions?: ValidationOptions diff --git a/src/api/validators/IsPassword.test.ts b/src/api/validators/IsPassword.test.ts deleted file mode 100644 index d25a77505..000000000 --- a/src/api/validators/IsPassword.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isPassword } from "./IsPassword"; - -describe("isPassword should return true", () => { - it("when given a valid password", () => { - expect(isPassword("Password1")).toBe(true); - }); -}); - -describe("should return false", () => { - it("when given a non-string", () => { - expect(isPassword(123)).toBe(false); - }); - - it("when given a string with length less than 8", () => { - expect(isPassword("Passwor")).toBe(false); - }); - - it("when given a string without lowercase letters", () => { - expect(isPassword("PASSWORD1")).toBe(false); - }); - - it("when given a string without uppercase letters", () => { - expect(isPassword("password1")).toBe(false); - }); - - it("when given a string without numbers", () => { - expect(isPassword("Password")).toBe(false); - }); -}); diff --git a/src/api/validators/IsPassword.ts b/src/api/validators/IsPassword.ts index 641ecab6f..c1157d3c2 100644 --- a/src/api/validators/IsPassword.ts +++ b/src/api/validators/IsPassword.ts @@ -1,15 +1,6 @@ +import { isPassword } from "@beabee/beabee-common"; import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; -export function isPassword(password: unknown): boolean { - return ( - typeof password === "string" && - password.length >= 8 && - /[a-z]/.test(password) && - /[A-Z]/.test(password) && - /[0-9]/.test(password) - ); -} - export default function IsPassword( validationOptions?: ValidationOptions ): PropertyDecorator { diff --git a/src/api/validators/IsSlug.test.ts b/src/api/validators/IsSlug.test.ts deleted file mode 100644 index 04e31494c..000000000 --- a/src/api/validators/IsSlug.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isSlug } from "./IsSlug"; - -describe("isSlug should return true", () => { - it("when given a valid slug", () => { - expect(isSlug("foo-bar")).toBe(true); - }); - - it("when given a string with uppercase letters", () => { - expect(isSlug("FooBar")).toBe(true); - }); - - it("when given a string with valid non-alphanumeric characters", () => { - expect(isSlug("foo_bar")).toBe(true); - }); -}); - -describe("should return false", () => { - it("when given a non-string", () => { - expect(isSlug(123)).toBe(false); - }); - - it("when given a string with forbidden characters", () => { - expect(isSlug("foo bar ?? &")).toBe(false); - }); -}); diff --git a/src/api/validators/IsSlug.ts b/src/api/validators/IsSlug.ts index 6ae18c773..efe225da6 100644 --- a/src/api/validators/IsSlug.ts +++ b/src/api/validators/IsSlug.ts @@ -1,9 +1,5 @@ +import { isSlug } from "@beabee/beabee-common"; import { buildMessage, ValidateBy, ValidationOptions } from "class-validator"; -import slugify from "slugify"; - -export function isSlug(slug: unknown): boolean { - return typeof slug === "string" && slug === slugify(slug); -} export default function IsSlug( validationOptions?: ValidationOptions diff --git a/src/api/validators/IsType.test.ts b/src/api/validators/IsType.test.ts deleted file mode 100644 index 69ab12188..000000000 --- a/src/api/validators/IsType.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isType } from "./IsType"; - -describe("isType should return true", () => { - it("when the value matches the type", () => { - expect(isType(["string"], "foo")).toBe(true); - }); - - it("when the value matches one of the types", () => { - expect(isType(["string", "number"], "foo")).toBe(true); - expect(isType(["string", "number"], 5)).toBe(true); - }); -}); - -describe("should return false", () => { - it("when the value does not match the type", () => { - expect(isType(["string"], 123)).toBe(false); - }); - - it("when the value does not match any of the types", () => { - expect(isType(["string", "number"], true)).toBe(false); - }); -}); diff --git a/src/api/validators/IsType.ts b/src/api/validators/IsType.ts index 0a04b4d72..4009d0869 100644 --- a/src/api/validators/IsType.ts +++ b/src/api/validators/IsType.ts @@ -1,22 +1,7 @@ +import { isType } from "@beabee/beabee-common"; import { ValidateBy, ValidationOptions } from "class-validator"; import { ValidationArguments } from "class-validator/types/validation/ValidationArguments"; -export function isType( - types: Array< - | "string" - | "number" - | "bigint" - | "boolean" - | "symbol" - | "undefined" - | "object" - | "function" - >, - value: unknown -): boolean { - return types.includes(typeof value); -} - export function IsType( types: Array< | "string" diff --git a/src/api/validators/IsValidPayFee.test.ts b/src/api/validators/IsValidPayFee.test.ts deleted file mode 100644 index 4eec9f880..000000000 --- a/src/api/validators/IsValidPayFee.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { isValidPayFee } from "./IsValidPayFee"; -import { ContributionPeriod } from "@beabee/beabee-common"; - -describe("isValidPayFee should return true", () => { - it("when given a valid pay fee", () => { - expect(isValidPayFee(true, 5, ContributionPeriod.Monthly)).toBe(true); - }); - it("when given a valid pay fee", () => { - expect(isValidPayFee(false, 20, ContributionPeriod.Monthly)).toBe(true); - }); - it("when given a valid pay fee", () => { - expect(isValidPayFee(false, 50, ContributionPeriod.Annually)).toBe(true); - }); -}); - -describe("should return false", () => { - it("when given invalid arguments non-boolean", () => { - expect(isValidPayFee("foo", "blah", "blah")).toBe(false); - }); - - it("when trying to pay a fee for an annual contribution", () => { - expect(isValidPayFee(true, 50, ContributionPeriod.Annually)).toBe(false); - }); - - it("when trying to not pay a fee for a monthly contribution of £1", () => { - expect(isValidPayFee(false, 1, ContributionPeriod.Monthly)).toBe(false); - }); -}); diff --git a/src/api/validators/IsValidPayFee.ts b/src/api/validators/IsValidPayFee.ts index d96260e86..da62e531f 100644 --- a/src/api/validators/IsValidPayFee.ts +++ b/src/api/validators/IsValidPayFee.ts @@ -1,38 +1,7 @@ -import { ContributionPeriod } from "@beabee/beabee-common"; -import { - ValidateBy, - ValidationOptions, - buildMessage, - isEnum -} from "class-validator"; +import { ContributionPeriod, isValidPayFee } from "@beabee/beabee-common"; +import { ValidateBy, ValidationOptions, buildMessage } from "class-validator"; import OptionsService from "@core/services/OptionsService"; -import { isNumber } from "lodash"; - -export function isValidPayFee( - value: unknown, - amount: unknown, - period: unknown -): boolean { - if ( - typeof value !== "boolean" || - !isEnum(period, ContributionPeriod) || - !isNumber(amount) - ) { - return false; - } - - // Annual contributions don't pay a fee - if (value && period === ContributionPeriod.Annually) { - return false; - } - // £1 monthly contributions must pay fee - if (!value && period === ContributionPeriod.Monthly && amount === 1) { - return false; - } - - return true; -} export default function IsValidPayFee( validationOptions?: ValidationOptions diff --git a/src/api/validators/MinContributionAmount.ts b/src/api/validators/MinContributionAmount.ts index a5ba9ffe7..a9def5f10 100644 --- a/src/api/validators/MinContributionAmount.ts +++ b/src/api/validators/MinContributionAmount.ts @@ -1,7 +1,6 @@ -import { ContributionPeriod } from "@beabee/beabee-common"; +import { ContributionPeriod, isPeriod } from "@beabee/beabee-common"; import { buildMessage, - isEnum, isNumber, ValidateBy, ValidationArguments, @@ -20,10 +19,6 @@ function getMinAmount(period: ContributionPeriod) { : minMonthlyAmount * 12; } -function isPeriod(period: unknown): period is ContributionPeriod { - return isEnum(period, ContributionPeriod); -} - function getPeriod( args: ValidationArguments | undefined ): ContributionPeriod | undefined { From a0b956ec71d79566c3bb6eef92046044472dc25c Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Thu, 15 Feb 2024 16:16:39 +0000 Subject: [PATCH 5/9] Use beabee-common v0.20.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32784c19e..119fb4d99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.8", "license": "GPL-3.0", "dependencies": { - "@beabee/beabee-common": "^0.20.0-alpha.3", + "@beabee/beabee-common": "^0.20.0", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", "ajv": "^8.12.0", @@ -850,9 +850,9 @@ "dev": true }, "node_modules/@beabee/beabee-common": { - "version": "0.20.0-alpha.3", - "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-0.20.0-alpha.3.tgz", - "integrity": "sha512-bl/Pw+1p+KGRSnwhugicNYzfVzopd+Qyz5hEbLg5g57MxoMGD6fexxZ1AU38ddUPFOIodsm9qXFilaIgr/zF6A==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-0.20.0.tgz", + "integrity": "sha512-KiqL0qKO9SxSreasLB/ZQGPNkjLwkEGmoZD6AZjZdWz3d+yYWLakuQEyZmLKfN7aVXZmMEwNBs8euB6gJGCdPg==", "dependencies": { "date-fns": "^3.2.0" } diff --git a/package.json b/package.json index daf8d1c72..b9e2a4a9c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test": "jest --setupFiles dotenv/config" }, "dependencies": { - "@beabee/beabee-common": "^0.20.0-alpha.3", + "@beabee/beabee-common": "^0.20.0", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", "ajv": "^8.12.0", From 84c9a7a957b4008220e5711da1637ce0260b4610 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Fri, 22 Mar 2024 15:32:41 +0000 Subject: [PATCH 6/9] Use old slugify to avoid slug changes for now --- src/api/validators/IsSlug.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/validators/IsSlug.ts b/src/api/validators/IsSlug.ts index efe225da6..d1c9179b5 100644 --- a/src/api/validators/IsSlug.ts +++ b/src/api/validators/IsSlug.ts @@ -1,5 +1,5 @@ -import { isSlug } from "@beabee/beabee-common"; import { buildMessage, ValidateBy, ValidationOptions } from "class-validator"; +import slugify from "slugify"; export default function IsSlug( validationOptions?: ValidationOptions @@ -8,7 +8,9 @@ export default function IsSlug( { name: "isSlug", validator: { - validate: isSlug, + validate(value) { + return typeof value === "string" && value === slugify(value); + }, defaultMessage: buildMessage( (eachPrefix) => eachPrefix + "$property must be a slug", validationOptions From 66fdda1fe0470a923ae895a738b035e6880d3ac7 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Fri, 22 Mar 2024 15:38:31 +0000 Subject: [PATCH 7/9] Fix invalid validator --- src/api/dto/CalloutFormDto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/dto/CalloutFormDto.ts b/src/api/dto/CalloutFormDto.ts index 8aaf7f0f0..b2a824a3a 100644 --- a/src/api/dto/CalloutFormDto.ts +++ b/src/api/dto/CalloutFormDto.ts @@ -148,7 +148,7 @@ class CalloutComponentInputSelectableDto extends CalloutComponentInputDto implements CalloutComponentBaseInputSelectableSchema { - @Equals(calloutComponentInputSelectableTypes) + @IsIn(calloutComponentInputSelectableTypes) type!: CalloutComponentBaseInputSelectableSchema["type"]; @ValidateNested({ each: true }) From 9844549c205ca3f77d2ce07ac6b4b1744d566c87 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Fri, 22 Mar 2024 16:38:16 +0000 Subject: [PATCH 8/9] Update to 0.20.2 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index aefcdee2a..b445d31d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.8", "license": "GPL-3.0", "dependencies": { - "@beabee/beabee-common": "^0.20.2-alpha.2", + "@beabee/beabee-common": "^0.20.2", "@captchafox/node": "^1.2.0", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", @@ -851,9 +851,9 @@ "dev": true }, "node_modules/@beabee/beabee-common": { - "version": "0.20.2-alpha.2", - "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-0.20.2-alpha.2.tgz", - "integrity": "sha512-+UPLlVyFuInCpnC3K84MSI/HIvFRDG7VpeRp8vhAq6LW6hAGbF9VivfbSlYKOUPRPXmkrDaShApGhCOdJt2TLw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@beabee/beabee-common/-/beabee-common-0.20.2.tgz", + "integrity": "sha512-UmR3EFSIXWbO1fp5OOYIV42j5uuTyPp0etXmQobEoNoBHW07obhsa/VUFhCX5CH006rTkJRchNfzu9/lJamDYQ==", "dependencies": { "date-fns": "^3.3.1" } diff --git a/package.json b/package.json index 1475f7eab..534eb540a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test": "jest --setupFiles dotenv/config" }, "dependencies": { - "@beabee/beabee-common": "^0.20.2-alpha.2", + "@beabee/beabee-common": "^0.20.2", "@captchafox/node": "^1.2.0", "@inquirer/prompts": "^3.3.0", "@sendgrid/mail": "^8.1.0", From e8f44358d5b5606789cd8ee43ba660a71eeb8310 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Fri, 22 Mar 2024 16:42:27 +0000 Subject: [PATCH 9/9] Use isCalloutComponentOfBaseType --- src/api/dto/CalloutFormDto.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/api/dto/CalloutFormDto.ts b/src/api/dto/CalloutFormDto.ts index b2a824a3a..862f04f62 100644 --- a/src/api/dto/CalloutFormDto.ts +++ b/src/api/dto/CalloutFormDto.ts @@ -17,7 +17,9 @@ import { SetCalloutNavigationSchema, SetCalloutSlideSchema, CalloutComponentBaseRules, - CalloutResponseAnswer + CalloutResponseAnswer, + isCalloutComponentOfBaseType, + CalloutComponentBaseType } from "@beabee/beabee-common"; import { Transform, Type, plainToInstance } from "class-transformer"; import { @@ -171,13 +173,25 @@ function ComponentType() { return plainToInstance(CalloutComponentInputSelectDto, component); } - if (calloutComponentInputSelectableTypes.includes(component.type)) { + if ( + isCalloutComponentOfBaseType( + component, + CalloutComponentBaseType.INPUT_SELECTABLE + ) + ) { return plainToInstance(CalloutComponentInputSelectableDto, component); } - if (calloutComponentInputTypes.includes(component.type)) { + if ( + isCalloutComponentOfBaseType(component, CalloutComponentBaseType.INPUT) + ) { return plainToInstance(CalloutComponentInputDto, component); } - if (calloutComponentNestableTypes.includes(component.type)) { + if ( + isCalloutComponentOfBaseType( + component, + CalloutComponentBaseType.NESTABLE + ) + ) { return plainToInstance(CalloutComponentNestableDto, component); }