From 1043391c0622810c6613390876a9bcd6209670de Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Tue, 28 Jan 2025 12:02:14 +0100 Subject: [PATCH 1/8] validation package --- README.md | 1 + packages/validation/README.md | 38 ++++++++++++++++++++++ packages/validation/package.json | 28 ++++++++++++++++ packages/validation/src/no.ts | 7 ++++ packages/validation/src/se.ts | 7 ++++ packages/validation/src/validation.test.ts | 30 +++++++++++++++++ packages/validation/tsconfig.json | 7 ++++ pnpm-lock.yaml | 2 ++ 8 files changed, 120 insertions(+) create mode 100644 packages/validation/README.md create mode 100644 packages/validation/package.json create mode 100644 packages/validation/src/no.ts create mode 100644 packages/validation/src/se.ts create mode 100644 packages/validation/src/validation.test.ts create mode 100644 packages/validation/tsconfig.json diff --git a/README.md b/README.md index ec19974..d0eab94 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This a monorepo of OBOS' open source frontend modules. ## Packages * [format](./packages/format) +* [validation](./packages/validation) ## Contributing diff --git a/packages/validation/README.md b/packages/validation/README.md new file mode 100644 index 0000000..e13386c --- /dev/null +++ b/packages/validation/README.md @@ -0,0 +1,38 @@ +# @obosbbl/validation + +[![NPM Version](https://img.shields.io/npm/v/%40obosbbl%2Fvalidation)](https://www.npmjs.com/package/@obosbbl/validation) + + +A collection of validation utilities for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. + +## Install + +```sh +# npm +npm install @obosbbl/validation + +# pnpm +pnpm add @obosbbl/validation +``` + +## Usage + +The package has two entrypoints, one for `no` and one for `se`. That allows you to import for only the locale you need. + + +```js +// πŸ‡³πŸ‡΄ example +import { postalCodeValidator } from '@obosbbl/validation/no'; +postalCodeValidator('0000') // => true +postalCodeValidator('00000') // => false + +// πŸ‡ΈπŸ‡ͺ example +import { postalCodeValidator } from '@obosbbl/validation/no'; +postalCodeValidator('00000') // => true +postalCodeValidator('00 000') // => true +postalCodeValidator('000 000') // => false +``` + +## Methods + +* validatePostalCode diff --git a/packages/validation/package.json b/packages/validation/package.json new file mode 100644 index 0000000..bf7abca --- /dev/null +++ b/packages/validation/package.json @@ -0,0 +1,28 @@ +{ + "name": "@obosbbl/validation", + "version": "0.0.0", + "description": "A collection of validation methods for OBOS", + "repository": { + "url": "https://github.com/code-obos/public-frontend-modules" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "exports": { + "./no": { + "types": "./dist/no.d.mts", + "default": "./dist/no.mjs" + }, + "./se": { + "types": "./dist/se.d.mts", + "default": "./dist/se.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "bunchee" + }, + "dependencies": {} +} diff --git a/packages/validation/src/no.ts b/packages/validation/src/no.ts new file mode 100644 index 0000000..c7b07fa --- /dev/null +++ b/packages/validation/src/no.ts @@ -0,0 +1,7 @@ +/** + * Validates the input value as a valid Norwegian postal (zip) code. + * Valid format is `0000`. + */ +export function postalCodeValidator(value: string): boolean { + return /^\d{4}$/.test(value); +} diff --git a/packages/validation/src/se.ts b/packages/validation/src/se.ts new file mode 100644 index 0000000..1bbdbed --- /dev/null +++ b/packages/validation/src/se.ts @@ -0,0 +1,7 @@ +/** + * Validates the input value as a valid Swedish postal (zip) code. + * Valid format is either `00 000` or `00000`. + */ +export function postalCodeValidator(value: string): boolean { + return /^\d{3} ?\d{2}$/.test(value); +} diff --git a/packages/validation/src/validation.test.ts b/packages/validation/src/validation.test.ts new file mode 100644 index 0000000..fd63d76 --- /dev/null +++ b/packages/validation/src/validation.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'vitest'; +import { postalCodeValidator as postalCodeValidatorNo } from './no'; +import { postalCodeValidator as postalCodeValidatorSe } from './se'; + +describe('Norwegian', () => { + describe('postalCodeValidator()', () => { + test('correctly validates postal codes', () => { + expect(postalCodeValidatorNo('1067')).toBeTruthy(); + + expect(postalCodeValidatorNo('1.67')).toBeFalsy(); + expect(postalCodeValidatorNo('10677')).toBeFalsy(); + expect(postalCodeValidatorNo(Number.NaN.toString())).toBeFalsy(); + expect(postalCodeValidatorNo('not a number')).toBeFalsy(); + }); + }); +}); + +describe('Swedish', () => { + describe('postalCodeValidator()', () => { + test('correctly validates postal codes', () => { + expect(postalCodeValidatorSe('100 26')).toBeTruthy(); + expect(postalCodeValidatorSe('10026')).toBeTruthy(); + + expect(postalCodeValidatorSe('100426')).toBeFalsy(); + expect(postalCodeValidatorSe('177')).toBeFalsy(); + expect(postalCodeValidatorSe(Number.NaN.toString())).toBeFalsy(); + expect(postalCodeValidatorSe('not a number')).toBeFalsy(); + }); + }); +}); diff --git a/packages/validation/tsconfig.json b/packages/validation/tsconfig.json new file mode 100644 index 0000000..dcbf63f --- /dev/null +++ b/packages/validation/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/*"], + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08bd49c..fa9b314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,8 @@ importers: packages/format: {} + packages/validation: {} + packages: '@andrewbranch/untar.js@1.0.3': From af27e241bc5af99f2785d241bd06f1e8fd2f275e Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Sat, 8 Feb 2025 08:17:17 +0100 Subject: [PATCH 2/8] phonenumber validator --- packages/validation/src/no.ts | 19 +++++++++ packages/validation/src/se.ts | 18 ++++++++ packages/validation/src/validation.test.ts | 49 +++++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/validation/src/no.ts b/packages/validation/src/no.ts index c7b07fa..f865283 100644 --- a/packages/validation/src/no.ts +++ b/packages/validation/src/no.ts @@ -5,3 +5,22 @@ export function postalCodeValidator(value: string): boolean { return /^\d{4}$/.test(value); } + +type PhoneNumberOptions = { + mobileOnly?: boolean; +}; + +export function phoneNumberValidator( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + const isPhoneNumber = /^\d{8}$/.test(value); + + if (options.mobileOnly) { + // Norwegian mobile phone numbers start with 4 or 9 + // See https://nkom.no/telefoni-og-telefonnummer/telefonnummer-og-den-norske-nummerplan/alle-nummerserier-for-norske-telefonnumre + return isPhoneNumber && ['4', '9'].includes(value.charAt(0)); + } + + return isPhoneNumber; +} diff --git a/packages/validation/src/se.ts b/packages/validation/src/se.ts index 1bbdbed..39b1571 100644 --- a/packages/validation/src/se.ts +++ b/packages/validation/src/se.ts @@ -5,3 +5,21 @@ export function postalCodeValidator(value: string): boolean { return /^\d{3} ?\d{2}$/.test(value); } + +type PhoneNumberOptions = { + mobileOnly?: boolean; +}; + +export function phoneNumberValidator( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + if (options.mobileOnly) { + const isMobileNumber = /^07\d{8}$/.test(value); + return isMobileNumber; + } + + const isPhoneNumber = /^0\d{7,9}$/.test(value); + + return isPhoneNumber; +} diff --git a/packages/validation/src/validation.test.ts b/packages/validation/src/validation.test.ts index fd63d76..afaae40 100644 --- a/packages/validation/src/validation.test.ts +++ b/packages/validation/src/validation.test.ts @@ -1,6 +1,12 @@ import { describe, expect, test } from 'vitest'; -import { postalCodeValidator as postalCodeValidatorNo } from './no'; -import { postalCodeValidator as postalCodeValidatorSe } from './se'; +import { + phoneNumberValidator as phoneNumberValidatorNo, + postalCodeValidator as postalCodeValidatorNo, +} from './no'; +import { + phoneNumberValidator as phoneNumberValidatorSe, + postalCodeValidator as postalCodeValidatorSe, +} from './se'; describe('Norwegian', () => { describe('postalCodeValidator()', () => { @@ -13,6 +19,23 @@ describe('Norwegian', () => { expect(postalCodeValidatorNo('not a number')).toBeFalsy(); }); }); + + describe('phoneNumberValidator()', () => { + test('validates phone numbers', () => { + expect(phoneNumberValidatorNo('22865500')).toBeTruthy(); + expect(phoneNumberValidatorNo('228655000')).toBeFalsy(); + + expect( + phoneNumberValidatorNo('40 00 00 00', { mobileOnly: true }), + ).toBeTruthy(); + expect( + phoneNumberValidatorNo('99 99 99 99', { mobileOnly: true }), + ).toBeTruthy(); + expect( + phoneNumberValidatorNo('22865500', { mobileOnly: true }), + ).toBeFalsy(); + }); + }); }); describe('Swedish', () => { @@ -27,4 +50,26 @@ describe('Swedish', () => { expect(postalCodeValidatorSe('not a number')).toBeFalsy(); }); }); + + describe('phoneNumberValidator()', () => { + test('validates phone numbers', () => { + // should be 8 to 10 digits + expect(phoneNumberValidatorSe('08123456')).toBeTruthy(); + expect(phoneNumberValidatorSe('031123456')).toBeTruthy(); + expect(phoneNumberValidatorSe('0311234567')).toBeTruthy(); + + // too short + expect(phoneNumberValidatorSe('0812345')).toBeFalsy(); + // too long + expect(phoneNumberValidatorSe('0303123456789')).toBeFalsy(); + + // cannot start with something other than 0 + expect(phoneNumberValidatorSe('12345678')).toBeFalsy(); + + // A Swedish mobile number is always 10 digits and starts with 07 + expect( + phoneNumberValidatorSe('0712345678', { mobileOnly: true }), + ).toBeTruthy(); + }); + }); }); From 8c429fb434206d3b670bcf3a64038b330759e9e9 Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Sun, 9 Feb 2025 10:38:24 +0100 Subject: [PATCH 3/8] add orgnumbervalidator --- packages/validation/src/no.ts | 26 ----- packages/validation/src/se.ts | 25 ----- packages/validation/src/validation.test.ts | 75 -------------- packages/{validation => validator}/README.md | 4 +- .../{validation => validator}/package.json | 6 +- packages/validator/src/no.ts | 68 +++++++++++++ packages/validator/src/se.ts | 58 +++++++++++ packages/validator/src/utils.ts | 28 ++++++ packages/validator/src/validation.test.ts | 99 +++++++++++++++++++ packages/validator/src/zod.test.ts | 22 +++++ .../{validation => validator}/tsconfig.json | 0 pnpm-lock.yaml | 11 ++- 12 files changed, 292 insertions(+), 130 deletions(-) delete mode 100644 packages/validation/src/no.ts delete mode 100644 packages/validation/src/se.ts delete mode 100644 packages/validation/src/validation.test.ts rename packages/{validation => validator}/README.md (85%) rename packages/{validation => validator}/package.json (86%) create mode 100644 packages/validator/src/no.ts create mode 100644 packages/validator/src/se.ts create mode 100644 packages/validator/src/utils.ts create mode 100644 packages/validator/src/validation.test.ts create mode 100644 packages/validator/src/zod.test.ts rename packages/{validation => validator}/tsconfig.json (100%) diff --git a/packages/validation/src/no.ts b/packages/validation/src/no.ts deleted file mode 100644 index f865283..0000000 --- a/packages/validation/src/no.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Validates the input value as a valid Norwegian postal (zip) code. - * Valid format is `0000`. - */ -export function postalCodeValidator(value: string): boolean { - return /^\d{4}$/.test(value); -} - -type PhoneNumberOptions = { - mobileOnly?: boolean; -}; - -export function phoneNumberValidator( - value: string, - options: PhoneNumberOptions = {}, -): boolean { - const isPhoneNumber = /^\d{8}$/.test(value); - - if (options.mobileOnly) { - // Norwegian mobile phone numbers start with 4 or 9 - // See https://nkom.no/telefoni-og-telefonnummer/telefonnummer-og-den-norske-nummerplan/alle-nummerserier-for-norske-telefonnumre - return isPhoneNumber && ['4', '9'].includes(value.charAt(0)); - } - - return isPhoneNumber; -} diff --git a/packages/validation/src/se.ts b/packages/validation/src/se.ts deleted file mode 100644 index 39b1571..0000000 --- a/packages/validation/src/se.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Validates the input value as a valid Swedish postal (zip) code. - * Valid format is either `00 000` or `00000`. - */ -export function postalCodeValidator(value: string): boolean { - return /^\d{3} ?\d{2}$/.test(value); -} - -type PhoneNumberOptions = { - mobileOnly?: boolean; -}; - -export function phoneNumberValidator( - value: string, - options: PhoneNumberOptions = {}, -): boolean { - if (options.mobileOnly) { - const isMobileNumber = /^07\d{8}$/.test(value); - return isMobileNumber; - } - - const isPhoneNumber = /^0\d{7,9}$/.test(value); - - return isPhoneNumber; -} diff --git a/packages/validation/src/validation.test.ts b/packages/validation/src/validation.test.ts deleted file mode 100644 index afaae40..0000000 --- a/packages/validation/src/validation.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { - phoneNumberValidator as phoneNumberValidatorNo, - postalCodeValidator as postalCodeValidatorNo, -} from './no'; -import { - phoneNumberValidator as phoneNumberValidatorSe, - postalCodeValidator as postalCodeValidatorSe, -} from './se'; - -describe('Norwegian', () => { - describe('postalCodeValidator()', () => { - test('correctly validates postal codes', () => { - expect(postalCodeValidatorNo('1067')).toBeTruthy(); - - expect(postalCodeValidatorNo('1.67')).toBeFalsy(); - expect(postalCodeValidatorNo('10677')).toBeFalsy(); - expect(postalCodeValidatorNo(Number.NaN.toString())).toBeFalsy(); - expect(postalCodeValidatorNo('not a number')).toBeFalsy(); - }); - }); - - describe('phoneNumberValidator()', () => { - test('validates phone numbers', () => { - expect(phoneNumberValidatorNo('22865500')).toBeTruthy(); - expect(phoneNumberValidatorNo('228655000')).toBeFalsy(); - - expect( - phoneNumberValidatorNo('40 00 00 00', { mobileOnly: true }), - ).toBeTruthy(); - expect( - phoneNumberValidatorNo('99 99 99 99', { mobileOnly: true }), - ).toBeTruthy(); - expect( - phoneNumberValidatorNo('22865500', { mobileOnly: true }), - ).toBeFalsy(); - }); - }); -}); - -describe('Swedish', () => { - describe('postalCodeValidator()', () => { - test('correctly validates postal codes', () => { - expect(postalCodeValidatorSe('100 26')).toBeTruthy(); - expect(postalCodeValidatorSe('10026')).toBeTruthy(); - - expect(postalCodeValidatorSe('100426')).toBeFalsy(); - expect(postalCodeValidatorSe('177')).toBeFalsy(); - expect(postalCodeValidatorSe(Number.NaN.toString())).toBeFalsy(); - expect(postalCodeValidatorSe('not a number')).toBeFalsy(); - }); - }); - - describe('phoneNumberValidator()', () => { - test('validates phone numbers', () => { - // should be 8 to 10 digits - expect(phoneNumberValidatorSe('08123456')).toBeTruthy(); - expect(phoneNumberValidatorSe('031123456')).toBeTruthy(); - expect(phoneNumberValidatorSe('0311234567')).toBeTruthy(); - - // too short - expect(phoneNumberValidatorSe('0812345')).toBeFalsy(); - // too long - expect(phoneNumberValidatorSe('0303123456789')).toBeFalsy(); - - // cannot start with something other than 0 - expect(phoneNumberValidatorSe('12345678')).toBeFalsy(); - - // A Swedish mobile number is always 10 digits and starts with 07 - expect( - phoneNumberValidatorSe('0712345678', { mobileOnly: true }), - ).toBeTruthy(); - }); - }); -}); diff --git a/packages/validation/README.md b/packages/validator/README.md similarity index 85% rename from packages/validation/README.md rename to packages/validator/README.md index e13386c..a1df279 100644 --- a/packages/validation/README.md +++ b/packages/validator/README.md @@ -3,7 +3,7 @@ [![NPM Version](https://img.shields.io/npm/v/%40obosbbl%2Fvalidation)](https://www.npmjs.com/package/@obosbbl/validation) -A collection of validation utilities for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. +A collection of validation utilities for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. Integrates neatly with [Zod](https://github.com/colinhacks/zod). ## Install @@ -36,3 +36,5 @@ postalCodeValidator('000 000') // => false ## Methods * validatePostalCode +* validatePhoneNumber +* validateOrganizationNumber diff --git a/packages/validation/package.json b/packages/validator/package.json similarity index 86% rename from packages/validation/package.json rename to packages/validator/package.json index bf7abca..94d2965 100644 --- a/packages/validation/package.json +++ b/packages/validator/package.json @@ -1,5 +1,5 @@ { - "name": "@obosbbl/validation", + "name": "@obosbbl/validator", "version": "0.0.0", "description": "A collection of validation methods for OBOS", "repository": { @@ -24,5 +24,7 @@ "scripts": { "build": "bunchee" }, - "dependencies": {} + "devDependencies": { + "zod": "^3.24.1" + } } diff --git a/packages/validator/src/no.ts b/packages/validator/src/no.ts new file mode 100644 index 0000000..53ef85b --- /dev/null +++ b/packages/validator/src/no.ts @@ -0,0 +1,68 @@ +import { mod11, stripFormatting } from './utils'; + +type PostalCodeOptions = { + /** + * Disallow formatting characters + * @default false + */ + strict?: boolean; +}; + +/** + * Validates the input value as a valid Norwegian postal (zip) code. + * Valid format is `0000`. + */ +export function validatePostalCode( + value: string, + options: PostalCodeOptions = {}, +): boolean { + if (!options.strict) { + value = stripFormatting(value); + } + + return /^\d{4}$/.test(value); +} + +type PhoneNumberOptions = { + /** + * Whether it should be a mobile number + * @default false + */ + mobileOnly?: boolean; + /** + * Disallow formatting characters + * @default false + */ + strict?: boolean; +}; + +export function validatePhoneNumber( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + if (!options.strict) { + value = stripFormatting(value); + } + + const isPhoneNumber = /^\d{8}$/.test(value); + + if (options.mobileOnly) { + // Norwegian mobile phone numbers start with 4 or 9 + // See https://nkom.no/telefoni-og-telefonnummer/telefonnummer-og-den-norske-nummerplan/alle-nummerserier-for-norske-telefonnumre + return isPhoneNumber && ['4', '9'].includes(value.charAt(0)); + } + + return isPhoneNumber; +} + +/** + * Validates the input value as a valid {@link https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ Norwegian organization number}. + * Valid format is 9 digits, spaces allowed, eg `000000000` or `000 000 000`. + */ +export function validateOrganizationNumber(value: string): boolean { + /** References: + * https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ + * https://no.wikipedia.org/wiki/Organisasjonsnummer + */ + return mod11(value, [3, 2, 7, 6, 5, 4, 3, 2]); +} diff --git a/packages/validator/src/se.ts b/packages/validator/src/se.ts new file mode 100644 index 0000000..87f7d57 --- /dev/null +++ b/packages/validator/src/se.ts @@ -0,0 +1,58 @@ +import { stripFormatting } from './utils'; + +type PostalCodeOptions = { + /** + * Disallow formatting characters + * @default false + */ + strict?: boolean; +}; + +/** + * Validates the input value as a valid Swedish postal (zip) code. + * Valid format is either `00 000` or `00000`. + */ +export function validatePostalCode( + value: string, + options: PostalCodeOptions = {}, +): boolean { + if (!options.strict) { + value = stripFormatting(value); + } + + return /^\d{3} ?\d{2}$/.test(value); +} + +type PhoneNumberOptions = { + mobileOnly?: boolean; + strict?: boolean; +}; + +export function validatePhoneNumber( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + if (!options.strict) { + value = stripFormatting(value); + } + + if (options.mobileOnly) { + const isMobileNumber = /^07\d{8}$/.test(value); + return isMobileNumber; + } + + const isPhoneNumber = /^0\d{7,9}$/.test(value); + + return isPhoneNumber; +} + +export function validateOrganizationNumber( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + if (!options.strict) { + value = stripFormatting(value); + } + + return /^\d{10}$/.test(value); +} diff --git a/packages/validator/src/utils.ts b/packages/validator/src/utils.ts new file mode 100644 index 0000000..a6296f4 --- /dev/null +++ b/packages/validator/src/utils.ts @@ -0,0 +1,28 @@ +/** Strip all formatting, leaving only numbers and letters */ +export function stripFormatting(value: string): string { + return value.replace(/[^a-zA-Z0-9]/g, ''); +} + +/** + * Used to validate Norwegian bank account numbers, organization numbers and national identity numbers. + * See https://no.wikipedia.org/wiki/MOD11 + */ +export function mod11(value: string, weights: number[]): boolean { + // Since the control digit is a single value, the lengths should be equal + if (weights.length + 1 !== value.length) { + return false; + } + + let sum = 0; + weights.forEach((weight, index) => { + sum += weight * Number(value[index]); + }); + + let controlNumber = 11 - (sum % 11); + + if (controlNumber === 11) { + controlNumber = 0; + } + + return controlNumber === Number(value[value.length - 1]); +} diff --git a/packages/validator/src/validation.test.ts b/packages/validator/src/validation.test.ts new file mode 100644 index 0000000..b13a7c3 --- /dev/null +++ b/packages/validator/src/validation.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, test } from 'vitest'; +import { + validateOrganizationNumber, + validatePhoneNumber as validatePhoneNumberNo, + validatePostalCode as validatePostalCodeNo, +} from './no'; +import { + validatePhoneNumber as validatePhoneNumberSe, + validatePostalCode as validatePostalCodeSe, +} from './se'; + +describe('Norwegian', () => { + describe('validatePostalCode()', () => { + test('correctly validates postal codes', () => { + expect(validatePostalCodeNo('1067')).toBeTruthy(); + + expect(validatePostalCodeNo('1.67')).toBeFalsy(); + expect(validatePostalCodeNo('10677')).toBeFalsy(); + expect(validatePostalCodeNo(Number.NaN.toString())).toBeFalsy(); + expect(validatePostalCodeNo('not a number')).toBeFalsy(); + }); + }); + + describe('validatePhoneNumber()', () => { + test('validates phone numbers', () => { + expect(validatePhoneNumberNo('22865500')).toBeTruthy(); + expect(validatePhoneNumberNo('228655000')).toBeFalsy(); + + expect( + validatePhoneNumberNo('40 00 00 00', { mobileOnly: true }), + ).toBeTruthy(); + expect( + validatePhoneNumberNo('99 99 99 99', { mobileOnly: true }), + ).toBeTruthy(); + expect( + validatePhoneNumberNo('22865500', { mobileOnly: true }), + ).toBeFalsy(); + }); + }); + + describe('orgNumberValidator()', () => { + test('correctly validates organization numbers', () => { + // Valid numbers generated here https://norske-testdata.no/orgnr/ + + expect(validateOrganizationNumber('737523063')).toBeTruthy(); + expect(validateOrganizationNumber('737 523 063')).toBeTruthy(); + + expect(validateOrganizationNumber('352317411')).toBeTruthy(); + expect(validateOrganizationNumber('352 317 411')).toBeTruthy(); + + expect(validateOrganizationNumber('306728156')).toBeTruthy(); + expect(validateOrganizationNumber('306 728 156')).toBeTruthy(); + + expect(validateOrganizationNumber('A52317411')).toBeFalsy(); + expect(validateOrganizationNumber('435 256 151')).toBeFalsy(); + expect(validateOrganizationNumber('435 256 156')).toBeFalsy(); + expect(validateOrganizationNumber('435 256 1564')).toBeFalsy(); + expect(validateOrganizationNumber('10')).toBeFalsy(); + expect(validateOrganizationNumber(Number.NaN.toString())).toBeFalsy(); + expect(validateOrganizationNumber('not a number')).toBeFalsy(); + }); + }); +}); + +describe('Swedish', () => { + describe('validatePostalCode', () => { + test('correctly validates postal codes', () => { + expect(validatePostalCodeSe('100 26')).toBeTruthy(); + expect(validatePostalCodeSe('10026')).toBeTruthy(); + + expect(validatePostalCodeSe('100426')).toBeFalsy(); + expect(validatePostalCodeSe('177')).toBeFalsy(); + expect(validatePostalCodeSe(Number.NaN.toString())).toBeFalsy(); + expect(validatePostalCodeSe('not a number')).toBeFalsy(); + }); + }); + + describe('validatePhoneNumber()', () => { + test('validates phone numbers', () => { + // should be 8 to 10 digits + expect(validatePhoneNumberSe('08123456')).toBeTruthy(); + expect(validatePhoneNumberSe('031123456')).toBeTruthy(); + expect(validatePhoneNumberSe('0311234567')).toBeTruthy(); + + // too short + expect(validatePhoneNumberSe('0812345')).toBeFalsy(); + // too long + expect(validatePhoneNumberSe('0303123456789')).toBeFalsy(); + + // cannot start with something other than 0 + expect(validatePhoneNumberSe('12345678')).toBeFalsy(); + + // A Swedish mobile number is always 10 digits and starts with 07 + expect( + validatePhoneNumberSe('0712345678', { mobileOnly: true }), + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/validator/src/zod.test.ts b/packages/validator/src/zod.test.ts new file mode 100644 index 0000000..c920079 --- /dev/null +++ b/packages/validator/src/zod.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { validatePhoneNumber } from './no'; + +test('it integrates with zod', () => { + const schema = z.object({ + name: z.string(), + phoneNumber: z + .string() + .refine( + (val) => validatePhoneNumber(val, { mobileOnly: true }), + 'Ugyldig telefonnummer', + ), + }); + + const data = { + name: 'Kari Nordmann', + phoneNumber: '92345678', + }; + + expect(schema.parse(data)).toEqual(data); +}); diff --git a/packages/validation/tsconfig.json b/packages/validator/tsconfig.json similarity index 100% rename from packages/validation/tsconfig.json rename to packages/validator/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9b314..a2c3dae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,11 @@ importers: packages/format: {} - packages/validation: {} + packages/validation: + devDependencies: + zod: + specifier: ^3.24.1 + version: 3.24.1 packages: @@ -1445,6 +1449,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + snapshots: '@andrewbranch/untar.js@1.0.3': {} @@ -2758,3 +2765,5 @@ snapshots: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + + zod@3.24.1: {} From 92eee4c0a84885cfdd75e5a6e759b7059f52eae2 Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Mon, 10 Feb 2025 12:40:05 +0100 Subject: [PATCH 4/8] validation package --- packages/validator/README.md | 20 ++- packages/validator/src/no.ts | 55 +++--- packages/validator/src/se.ts | 53 ++++-- packages/validator/src/types.ts | 7 + packages/validator/src/validation.test.ts | 198 +++++++++++++--------- 5 files changed, 213 insertions(+), 120 deletions(-) create mode 100644 packages/validator/src/types.ts diff --git a/packages/validator/README.md b/packages/validator/README.md index a1df279..4407a55 100644 --- a/packages/validator/README.md +++ b/packages/validator/README.md @@ -3,7 +3,7 @@ [![NPM Version](https://img.shields.io/npm/v/%40obosbbl%2Fvalidation)](https://www.npmjs.com/package/@obosbbl/validation) -A collection of validation utilities for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. Integrates neatly with [Zod](https://github.com/colinhacks/zod). +A collection of validation methods for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. Integrates neatly with [Zod](https://github.com/colinhacks/zod). ## Install @@ -24,17 +24,35 @@ The package has two entrypoints, one for `no` and one for `se`. That allows you // πŸ‡³πŸ‡΄ example import { postalCodeValidator } from '@obosbbl/validation/no'; postalCodeValidator('0000') // => true + postalCodeValidator('00000') // => false // πŸ‡ΈπŸ‡ͺ example import { postalCodeValidator } from '@obosbbl/validation/no'; postalCodeValidator('00000') // => true postalCodeValidator('00 000') // => true + postalCodeValidator('000 000') // => false ``` +## Strictness + +By default, the methods allows formatting characters when validating. If you need strict validation, you can pass the `strict` option to the method. + +```js +import { validateOrganizationNumber } from '@obosbbl/validation/no'; + +validateOrganizationNumber('937 052 766') // true; + +validateOrganizationNumber('937052766', { strict: true }) // true; +validateOrganizationNumber('937 052 766', { strict: true }) // false; +``` + +```js + ## Methods * validatePostalCode * validatePhoneNumber + * supports mobileOnly option * validateOrganizationNumber diff --git a/packages/validator/src/no.ts b/packages/validator/src/no.ts index 53ef85b..27b14fb 100644 --- a/packages/validator/src/no.ts +++ b/packages/validator/src/no.ts @@ -1,46 +1,49 @@ +import type { ValidatorOptions } from './types'; import { mod11, stripFormatting } from './utils'; -type PostalCodeOptions = { - /** - * Disallow formatting characters - * @default false - */ - strict?: boolean; -}; +type PostalCodeOptions = ValidatorOptions; /** - * Validates the input value as a valid Norwegian postal (zip) code. - * Valid format is `0000`. + * Validates that the input value is a Norwegian postal (zip) code. + * @example + * ``` + * validatePostalCode('0000') // => true + * ``` */ export function validatePostalCode( value: string, options: PostalCodeOptions = {}, ): boolean { - if (!options.strict) { + if (options.allowFormatting) { value = stripFormatting(value); } return /^\d{4}$/.test(value); } -type PhoneNumberOptions = { +type PhoneNumberOptions = ValidatorOptions & { /** * Whether it should be a mobile number * @default false */ mobileOnly?: boolean; - /** - * Disallow formatting characters - * @default false - */ - strict?: boolean; }; +/** + * Validates that the input value is a Norwegian phone number. + * + * Supports mobile only validation. + * @example + * ``` + * validatePhoneNumber('00000000') // => true + * validatePhoneNumber('90000000', { mobileOnly: true }) // => true + * ``` + */ export function validatePhoneNumber( value: string, options: PhoneNumberOptions = {}, ): boolean { - if (!options.strict) { + if (options.allowFormatting) { value = stripFormatting(value); } @@ -55,11 +58,23 @@ export function validatePhoneNumber( return isPhoneNumber; } +type OrganizationNumberOptions = ValidatorOptions; + /** - * Validates the input value as a valid {@link https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ Norwegian organization number}. - * Valid format is 9 digits, spaces allowed, eg `000000000` or `000 000 000`. + * Validates that the input value is a {@link https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ Norwegian organization number}. + * @example + * ``` + * validateOrganizationNumber('000000000') // => true + * ``` */ -export function validateOrganizationNumber(value: string): boolean { +export function validateOrganizationNumber( + value: string, + options: PhoneNumberOptions = {}, +): boolean { + if (options.allowFormatting) { + value = stripFormatting(value); + } + /** References: * https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ * https://no.wikipedia.org/wiki/Organisasjonsnummer diff --git a/packages/validator/src/se.ts b/packages/validator/src/se.ts index 87f7d57..cf09a97 100644 --- a/packages/validator/src/se.ts +++ b/packages/validator/src/se.ts @@ -1,38 +1,51 @@ +import type { ValidatorOptions } from './types'; import { stripFormatting } from './utils'; -type PostalCodeOptions = { - /** - * Disallow formatting characters - * @default false - */ - strict?: boolean; -}; +type PostalCodeOptions = ValidatorOptions; /** - * Validates the input value as a valid Swedish postal (zip) code. - * Valid format is either `00 000` or `00000`. + * Validates that the input value is a Swedish postal (zip) code. + * @example + * ``` + * validatePostalCode('00000') // => true + * ``` */ export function validatePostalCode( value: string, options: PostalCodeOptions = {}, ): boolean { - if (!options.strict) { + if (options.allowFormatting) { value = stripFormatting(value); } return /^\d{3} ?\d{2}$/.test(value); } -type PhoneNumberOptions = { +type PhoneNumberOptions = ValidatorOptions & { + /** + * Whether it should be a mobile number + * @default false + */ mobileOnly?: boolean; - strict?: boolean; }; +/** + * Validates that the input value is a Swedish phone number. + * + * Supports mobile only validation. + * @example + * ``` + * validatePhoneNumber('00000000') // => true + * validatePhoneNumber('000000000') // => true + * validatePhoneNumber('0000000000') // => true + * validatePhoneNumber('0700000000', { mobileOnly: true }) // => true + * ``` + */ export function validatePhoneNumber( value: string, options: PhoneNumberOptions = {}, ): boolean { - if (!options.strict) { + if (options.allowFormatting) { value = stripFormatting(value); } @@ -46,11 +59,21 @@ export function validatePhoneNumber( return isPhoneNumber; } +type OrganizationNumberOptions = ValidatorOptions; + +/** + * Validates that the input value is a {@link https://www.skatteverket.se/foretagochorganisationer/foretagare/startaochregistrera/organisationsnummer.4.361dc8c15312eff6fd235d1.html Swedish organization number}. + * @example + * ``` + * validateOrganizationNumber('000000000') // => true + * ``` + */ export function validateOrganizationNumber( value: string, - options: PhoneNumberOptions = {}, + options: OrganizationNumberOptions = {}, ): boolean { - if (!options.strict) { + // TODO: Implement proper validation + if (options.allowFormatting) { value = stripFormatting(value); } diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts new file mode 100644 index 0000000..19aa395 --- /dev/null +++ b/packages/validator/src/types.ts @@ -0,0 +1,7 @@ +export type ValidatorOptions = { + /** + * Allow formatting characters + * @default false + */ + allowFormatting?: boolean; +}; diff --git a/packages/validator/src/validation.test.ts b/packages/validator/src/validation.test.ts index b13a7c3..9f6f1fa 100644 --- a/packages/validator/src/validation.test.ts +++ b/packages/validator/src/validation.test.ts @@ -1,99 +1,129 @@ import { describe, expect, test } from 'vitest'; -import { - validateOrganizationNumber, - validatePhoneNumber as validatePhoneNumberNo, - validatePostalCode as validatePostalCodeNo, -} from './no'; -import { - validatePhoneNumber as validatePhoneNumberSe, - validatePostalCode as validatePostalCodeSe, -} from './se'; - -describe('Norwegian', () => { - describe('validatePostalCode()', () => { - test('correctly validates postal codes', () => { - expect(validatePostalCodeNo('1067')).toBeTruthy(); - - expect(validatePostalCodeNo('1.67')).toBeFalsy(); - expect(validatePostalCodeNo('10677')).toBeFalsy(); - expect(validatePostalCodeNo(Number.NaN.toString())).toBeFalsy(); - expect(validatePostalCodeNo('not a number')).toBeFalsy(); - }); - }); +import * as no from './no'; +import * as se from './se'; + +describe('no', () => { + test.each([ + ['0179', true], - describe('validatePhoneNumber()', () => { - test('validates phone numbers', () => { - expect(validatePhoneNumberNo('22865500')).toBeTruthy(); - expect(validatePhoneNumberNo('228655000')).toBeFalsy(); - - expect( - validatePhoneNumberNo('40 00 00 00', { mobileOnly: true }), - ).toBeTruthy(); - expect( - validatePhoneNumberNo('99 99 99 99', { mobileOnly: true }), - ).toBeTruthy(); - expect( - validatePhoneNumberNo('22865500', { mobileOnly: true }), - ).toBeFalsy(); - }); + ['01790', false], + ['not a number', false], + ])('validatePostalCode()', (input, expected) => { + expect(no.validatePostalCode(input)).toBe(expected); }); - describe('orgNumberValidator()', () => { - test('correctly validates organization numbers', () => { - // Valid numbers generated here https://norske-testdata.no/orgnr/ + test.each([ + ['22865500', true], + ['90000000', true], + ['40000000', true], + ['22 86 55 00', false], + ['000', false], + ['000000000', false], - expect(validateOrganizationNumber('737523063')).toBeTruthy(); - expect(validateOrganizationNumber('737 523 063')).toBeTruthy(); + // formatting + ['22 86 55 00', false, { allowFormatting: false }], + ['22 86 55 00', true, { allowFormatting: true }], - expect(validateOrganizationNumber('352317411')).toBeTruthy(); - expect(validateOrganizationNumber('352 317 411')).toBeTruthy(); + // mobile only + ['22865500', false, { mobileOnly: true }], + ['90000000', true, { mobileOnly: true }], + ['40000000', true, { mobileOnly: true }], + ])('validatePhoneNumber()', (input, expected, options) => { + expect(no.validatePhoneNumber(input, options)).toBe(expected); + }); - expect(validateOrganizationNumber('306728156')).toBeTruthy(); - expect(validateOrganizationNumber('306 728 156')).toBeTruthy(); + test.each([ + ['937052766', true], + ['937 052 766', false], + ['435256151', false], + ['not a number', false], + ['A37 052 766', false], - expect(validateOrganizationNumber('A52317411')).toBeFalsy(); - expect(validateOrganizationNumber('435 256 151')).toBeFalsy(); - expect(validateOrganizationNumber('435 256 156')).toBeFalsy(); - expect(validateOrganizationNumber('435 256 1564')).toBeFalsy(); - expect(validateOrganizationNumber('10')).toBeFalsy(); - expect(validateOrganizationNumber(Number.NaN.toString())).toBeFalsy(); - expect(validateOrganizationNumber('not a number')).toBeFalsy(); - }); + // formatting + ['937 052 766', false, { allowFormatting: false }], + ['937 052 766', true, { allowFormatting: true }], + ])('validateOrganizationNumber()', (input, expected) => { + expect(no.validateOrganizationNumber(input)).toBe(expected); }); }); -describe('Swedish', () => { - describe('validatePostalCode', () => { - test('correctly validates postal codes', () => { - expect(validatePostalCodeSe('100 26')).toBeTruthy(); - expect(validatePostalCodeSe('10026')).toBeTruthy(); - - expect(validatePostalCodeSe('100426')).toBeFalsy(); - expect(validatePostalCodeSe('177')).toBeFalsy(); - expect(validatePostalCodeSe(Number.NaN.toString())).toBeFalsy(); - expect(validatePostalCodeSe('not a number')).toBeFalsy(); - }); +describe('se', () => { + test.each([ + ['00000', true], + ['000 00', true], + + // strictness + ['000 00', false, { strict: true }], + + ['00', false], + ['not a number', false], + ])('validatePostalCode()', (input, expected) => { + expect(se.validatePostalCode(input)).toBe(expected); + }); + + test.each([ + ['22865500', true], + ['22 86 55 00', true], + ['90000000', true], + ['40000000', true], + ['000', false], + ['000000000', false], + + // strictness + ['22 86 55 00', false, { strict: true }], + + // mobile only + ['22 86 55 00', false, { mobileOnly: true }], + ['900 00 000', true, { mobileOnly: true }], + ['400 00 000', true, { mobileOnly: true }], + ])('validatePhoneNumber()', (input, expected, options) => { + expect(se.validatePhoneNumber(input, options)).toBe(expected); }); - describe('validatePhoneNumber()', () => { - test('validates phone numbers', () => { - // should be 8 to 10 digits - expect(validatePhoneNumberSe('08123456')).toBeTruthy(); - expect(validatePhoneNumberSe('031123456')).toBeTruthy(); - expect(validatePhoneNumberSe('0311234567')).toBeTruthy(); - - // too short - expect(validatePhoneNumberSe('0812345')).toBeFalsy(); - // too long - expect(validatePhoneNumberSe('0303123456789')).toBeFalsy(); - - // cannot start with something other than 0 - expect(validatePhoneNumberSe('12345678')).toBeFalsy(); - - // A Swedish mobile number is always 10 digits and starts with 07 - expect( - validatePhoneNumberSe('0712345678', { mobileOnly: true }), - ).toBeTruthy(); - }); + test.each([ + ['937052766', true], + ['937 052 766', true], + + ['A37 052 766', false], + ['435 256 151', false], + ['not a number', false], + ])('validateOrganizationNumber()', (input, expected) => { + expect(se.validateOrganizationNumber(input)).toBe(expected); }); }); + +// describe('no', () => { +// describe('validatePostalCode', () => { +// test('correctly validates postal codes', () => { +// expect(validatePostalCodeSe('100 26')).toBeTruthy(); +// expect(validatePostalCodeSe('10026')).toBeTruthy(); + +// expect(validatePostalCodeSe('100426')).toBeFalsy(); +// expect(validatePostalCodeSe('177')).toBeFalsy(); +// expect(validatePostalCodeSe(Number.NaN.toString())).toBeFalsy(); +// expect(validatePostalCodeSe('not a number')).toBeFalsy(); +// }); +// }); + +// describe('validatePhoneNumber()', () => { +// test('validates phone numbers', () => { +// // should be 8 to 10 digits +// expect(validatePhoneNumberSe('08123456')).toBeTruthy(); +// expect(validatePhoneNumberSe('031123456')).toBeTruthy(); +// expect(validatePhoneNumberSe('0311234567')).toBeTruthy(); + +// // too short +// expect(validatePhoneNumberSe('0812345')).toBeFalsy(); +// // too long +// expect(validatePhoneNumberSe('0303123456789')).toBeFalsy(); + +// // cannot start with something other than 0 +// expect(validatePhoneNumberSe('12345678')).toBeFalsy(); + +// // A Swedish mobile number is always 10 digits and starts with 07 +// expect( +// validatePhoneNumberSe('0712345678', { mobileOnly: true }), +// ).toBeTruthy(); +// }); +// }); +// }); From dc920b98a2783f6f5b30d3b6b2541809adecd587 Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Mon, 10 Feb 2025 13:42:00 +0100 Subject: [PATCH 5/8] getting tehre --- packages/validator/README.md | 69 +++++++++--- packages/validator/package.json | 3 - packages/validator/src/no.ts | 3 + packages/validator/src/se.ts | 7 +- packages/validator/src/validation.test.ts | 122 ++++++++-------------- packages/validator/src/zod.test.ts | 22 ---- pnpm-lock.yaml | 11 +- 7 files changed, 103 insertions(+), 134 deletions(-) delete mode 100644 packages/validator/src/zod.test.ts diff --git a/packages/validator/README.md b/packages/validator/README.md index 4407a55..af96695 100644 --- a/packages/validator/README.md +++ b/packages/validator/README.md @@ -3,7 +3,7 @@ [![NPM Version](https://img.shields.io/npm/v/%40obosbbl%2Fvalidation)](https://www.npmjs.com/package/@obosbbl/validation) -A collection of validation methods for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. Integrates neatly with [Zod](https://github.com/colinhacks/zod). +A collection of validation methods for both πŸ‡³πŸ‡΄ and πŸ‡ΈπŸ‡ͺ with zero dependencies. ## Install @@ -22,37 +22,72 @@ The package has two entrypoints, one for `no` and one for `se`. That allows you ```js // πŸ‡³πŸ‡΄ example -import { postalCodeValidator } from '@obosbbl/validation/no'; -postalCodeValidator('0000') // => true +import { validateOrganizationNumber } from '@obosbbl/validation/no'; +validateOrganizationNumber('937052766') // => true -postalCodeValidator('00000') // => false +validateOrganizationNumber('000') // => false // πŸ‡ΈπŸ‡ͺ example -import { postalCodeValidator } from '@obosbbl/validation/no'; -postalCodeValidator('00000') // => true -postalCodeValidator('00 000') // => true +import { validateOrganizationNumer } from '@obosbbl/validation/se'; +validateOrganizationNumber('5592221054') // => true -postalCodeValidator('000 000') // => false +validateOrganizationNumber('000') // => false ``` -## Strictness +## Strictness and formatting characters + +The methods are "strict" by default, meaning no formatting characters in the input is allowed. +This is preferrable, for instance when doing server-side validation, where the input is often expected to be a "clean" value. + +If you want to allow formatting characters in the input, you can pass `allowFormatting: true` in the options object to the method. +Note that this currently allows any formatting characters, not just the just the "expected" ones for the input type. -By default, the methods allows formatting characters when validating. If you need strict validation, you can pass the `strict` option to the method. ```js import { validateOrganizationNumber } from '@obosbbl/validation/no'; -validateOrganizationNumber('937 052 766') // true; - -validateOrganizationNumber('937052766', { strict: true }) // true; -validateOrganizationNumber('937 052 766', { strict: true }) // false; +validateOrganizationNumber('937052766') // true +// formatting characters disallowed by default +validateOrganizationNumber('937 052 766') // false; +// allow formatting characters +validateOrganizationNumber('937 052 766', { allowFormatting: true }) // true; ``` -```js - ## Methods * validatePostalCode * validatePhoneNumber - * supports mobileOnly option + * supports mobileOnly option * validateOrganizationNumber + * Check digit verification is currently only implemented for Norwegian organization numbers. For Swedish organiation numbers, we only check the length of the input. PRs are welcome to fix this. + +## Example usage with Zod + +```js +import { z } from 'zod'; +import { validatePhoneNumber } from '@obosbbl/validation/no'; + +const mobileOnlySchema = z.object({ + name: z.string(), + phoneNumber: z + .string() + .refine( + (val) => validatePhoneNumber(val, { mobileOnly: true }), + 'Telefonnummeret er ikke et gyldig mobilnummer', + ), +}); + +const validData = { + name: 'Kari Nordmann', + phoneNumber: '92345678', +}; + +mobileOnlySchema.parse(validData); // => { name: 'Kari Nordmann', phoneNumber: '92345678' } + +const invalidData = { + name: 'Ola Nordmann', + phoneNumber: '22865500', +} + +mobileOnlySchema.parse(invalidData); // => throws ZodError +``` diff --git a/packages/validator/package.json b/packages/validator/package.json index 94d2965..04976bd 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -23,8 +23,5 @@ ], "scripts": { "build": "bunchee" - }, - "devDependencies": { - "zod": "^3.24.1" } } diff --git a/packages/validator/src/no.ts b/packages/validator/src/no.ts index 27b14fb..ccf200c 100644 --- a/packages/validator/src/no.ts +++ b/packages/validator/src/no.ts @@ -15,6 +15,7 @@ export function validatePostalCode( options: PostalCodeOptions = {}, ): boolean { if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } @@ -44,6 +45,7 @@ export function validatePhoneNumber( options: PhoneNumberOptions = {}, ): boolean { if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } @@ -72,6 +74,7 @@ export function validateOrganizationNumber( options: PhoneNumberOptions = {}, ): boolean { if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } diff --git a/packages/validator/src/se.ts b/packages/validator/src/se.ts index cf09a97..a96f2d1 100644 --- a/packages/validator/src/se.ts +++ b/packages/validator/src/se.ts @@ -15,10 +15,11 @@ export function validatePostalCode( options: PostalCodeOptions = {}, ): boolean { if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } - return /^\d{3} ?\d{2}$/.test(value); + return /^\d{5}$/.test(value); } type PhoneNumberOptions = ValidatorOptions & { @@ -46,6 +47,7 @@ export function validatePhoneNumber( options: PhoneNumberOptions = {}, ): boolean { if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } @@ -72,8 +74,9 @@ export function validateOrganizationNumber( value: string, options: OrganizationNumberOptions = {}, ): boolean { - // TODO: Implement proper validation + // TODO: Implement checksum validation. For now it only checks the number of digits. if (options.allowFormatting) { + // biome-ignore lint/style/noParameterAssign: value = stripFormatting(value); } diff --git a/packages/validator/src/validation.test.ts b/packages/validator/src/validation.test.ts index 9f6f1fa..7e307b7 100644 --- a/packages/validator/src/validation.test.ts +++ b/packages/validator/src/validation.test.ts @@ -5,20 +5,19 @@ import * as se from './se'; describe('no', () => { test.each([ ['0179', true], - ['01790', false], ['not a number', false], - ])('validatePostalCode()', (input, expected) => { + ])('validatePostalCode(%s) -> %s', (input, expected) => { expect(no.validatePostalCode(input)).toBe(expected); }); test.each([ - ['22865500', true], - ['90000000', true], - ['40000000', true], - ['22 86 55 00', false], - ['000', false], - ['000000000', false], + ['22865500', true, undefined], + ['90000000', true, undefined], + ['40000000', true, undefined], + ['22 86 55 00', false, undefined], + ['000', false, undefined], + ['000000000', false, undefined], // formatting ['22 86 55 00', false, { allowFormatting: false }], @@ -28,102 +27,65 @@ describe('no', () => { ['22865500', false, { mobileOnly: true }], ['90000000', true, { mobileOnly: true }], ['40000000', true, { mobileOnly: true }], - ])('validatePhoneNumber()', (input, expected, options) => { + ])('validatePhoneNumber(%s) -> %s', (input, expected, options) => { expect(no.validatePhoneNumber(input, options)).toBe(expected); }); test.each([ - ['937052766', true], - ['937 052 766', false], - ['435256151', false], - ['not a number', false], - ['A37 052 766', false], + ['937052766', true, undefined], + ['937 052 766', false, undefined], + ['435256151', false, undefined], + ['not a number', false, undefined], + ['A37 052 766', false, undefined], // formatting ['937 052 766', false, { allowFormatting: false }], ['937 052 766', true, { allowFormatting: true }], - ])('validateOrganizationNumber()', (input, expected) => { - expect(no.validateOrganizationNumber(input)).toBe(expected); + ])('validateOrganizationNumber(%s) -> %s', (input, expected, options) => { + expect(no.validateOrganizationNumber(input, options)).toBe(expected); }); }); describe('se', () => { test.each([ - ['00000', true], - ['000 00', true], + ['00000', true, undefined], + ['000 00', false, undefined], + ['not a number', false, undefined], - // strictness - ['000 00', false, { strict: true }], - - ['00', false], - ['not a number', false], - ])('validatePostalCode()', (input, expected) => { - expect(se.validatePostalCode(input)).toBe(expected); + // formatting + ['000 00', false, { allowFormatting: false }], + ['000 00', true, { allowFormatting: true }], + ])('validatePostalCode(%s) -> %s', (input, expected, options) => { + expect(se.validatePostalCode(input, options)).toBe(expected); }); test.each([ - ['22865500', true], - ['22 86 55 00', true], - ['90000000', true], - ['40000000', true], - ['000', false], - ['000000000', false], + ['08123456', true, undefined], + ['0812345', false, undefined], + ['031123456', true, undefined], + ['030312345678', false, undefined], - // strictness - ['22 86 55 00', false, { strict: true }], + // formatting + ['031-123 45', true, { allowFormatting: true }], + ['031-123 45', false, { allowFormatting: false }], // mobile only - ['22 86 55 00', false, { mobileOnly: true }], - ['900 00 000', true, { mobileOnly: true }], - ['400 00 000', true, { mobileOnly: true }], - ])('validatePhoneNumber()', (input, expected, options) => { + ['0701234567', true, { mobileOnly: true }], + ['031123456', false, { mobileOnly: true }], + ])('validatePhoneNumber(%s) -> %s', (input, expected, options) => { expect(se.validatePhoneNumber(input, options)).toBe(expected); }); test.each([ - ['937052766', true], - ['937 052 766', true], + ['5592221054', true, undefined], + ['559222-1054', false, undefined], + ['559222105', false, undefined], + ['55922210546', false, undefined], - ['A37 052 766', false], - ['435 256 151', false], - ['not a number', false], - ])('validateOrganizationNumber()', (input, expected) => { - expect(se.validateOrganizationNumber(input)).toBe(expected); + // formatting + ['559222-1054', false, { allowFormatting: false }], + ['559222-1054', true, { allowFormatting: true }], + ])('validateOrganizationNumber(%s) -> %s', (input, expected, options) => { + expect(se.validateOrganizationNumber(input, options)).toBe(expected); }); }); - -// describe('no', () => { -// describe('validatePostalCode', () => { -// test('correctly validates postal codes', () => { -// expect(validatePostalCodeSe('100 26')).toBeTruthy(); -// expect(validatePostalCodeSe('10026')).toBeTruthy(); - -// expect(validatePostalCodeSe('100426')).toBeFalsy(); -// expect(validatePostalCodeSe('177')).toBeFalsy(); -// expect(validatePostalCodeSe(Number.NaN.toString())).toBeFalsy(); -// expect(validatePostalCodeSe('not a number')).toBeFalsy(); -// }); -// }); - -// describe('validatePhoneNumber()', () => { -// test('validates phone numbers', () => { -// // should be 8 to 10 digits -// expect(validatePhoneNumberSe('08123456')).toBeTruthy(); -// expect(validatePhoneNumberSe('031123456')).toBeTruthy(); -// expect(validatePhoneNumberSe('0311234567')).toBeTruthy(); - -// // too short -// expect(validatePhoneNumberSe('0812345')).toBeFalsy(); -// // too long -// expect(validatePhoneNumberSe('0303123456789')).toBeFalsy(); - -// // cannot start with something other than 0 -// expect(validatePhoneNumberSe('12345678')).toBeFalsy(); - -// // A Swedish mobile number is always 10 digits and starts with 07 -// expect( -// validatePhoneNumberSe('0712345678', { mobileOnly: true }), -// ).toBeTruthy(); -// }); -// }); -// }); diff --git a/packages/validator/src/zod.test.ts b/packages/validator/src/zod.test.ts deleted file mode 100644 index c920079..0000000 --- a/packages/validator/src/zod.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { z } from 'zod'; -import { validatePhoneNumber } from './no'; - -test('it integrates with zod', () => { - const schema = z.object({ - name: z.string(), - phoneNumber: z - .string() - .refine( - (val) => validatePhoneNumber(val, { mobileOnly: true }), - 'Ugyldig telefonnummer', - ), - }); - - const data = { - name: 'Kari Nordmann', - phoneNumber: '92345678', - }; - - expect(schema.parse(data)).toEqual(data); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2c3dae..897955a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,11 +29,7 @@ importers: packages/format: {} - packages/validation: - devDependencies: - zod: - specifier: ^3.24.1 - version: 3.24.1 + packages/validator: {} packages: @@ -1449,9 +1445,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - zod@3.24.1: - resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} - snapshots: '@andrewbranch/untar.js@1.0.3': {} @@ -2765,5 +2758,3 @@ snapshots: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - - zod@3.24.1: {} From 20f54d76eee80f22989e1ba6f138f6982bfcb861 Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Mon, 10 Feb 2025 13:44:28 +0100 Subject: [PATCH 6/8] cleanup --- packages/{validator => validation}/README.md | 0 packages/{validator => validation}/package.json | 2 +- packages/{validator => validation}/src/no.ts | 0 packages/{validator => validation}/src/se.ts | 0 packages/{validator => validation}/src/types.ts | 0 packages/{validator => validation}/src/utils.ts | 0 packages/{validator => validation}/src/validation.test.ts | 0 packages/{validator => validation}/tsconfig.json | 0 pnpm-lock.yaml | 2 +- 9 files changed, 2 insertions(+), 2 deletions(-) rename packages/{validator => validation}/README.md (100%) rename packages/{validator => validation}/package.json (93%) rename packages/{validator => validation}/src/no.ts (100%) rename packages/{validator => validation}/src/se.ts (100%) rename packages/{validator => validation}/src/types.ts (100%) rename packages/{validator => validation}/src/utils.ts (100%) rename packages/{validator => validation}/src/validation.test.ts (100%) rename packages/{validator => validation}/tsconfig.json (100%) diff --git a/packages/validator/README.md b/packages/validation/README.md similarity index 100% rename from packages/validator/README.md rename to packages/validation/README.md diff --git a/packages/validator/package.json b/packages/validation/package.json similarity index 93% rename from packages/validator/package.json rename to packages/validation/package.json index 04976bd..4416cd9 100644 --- a/packages/validator/package.json +++ b/packages/validation/package.json @@ -1,5 +1,5 @@ { - "name": "@obosbbl/validator", + "name": "@obosbbl/validation", "version": "0.0.0", "description": "A collection of validation methods for OBOS", "repository": { diff --git a/packages/validator/src/no.ts b/packages/validation/src/no.ts similarity index 100% rename from packages/validator/src/no.ts rename to packages/validation/src/no.ts diff --git a/packages/validator/src/se.ts b/packages/validation/src/se.ts similarity index 100% rename from packages/validator/src/se.ts rename to packages/validation/src/se.ts diff --git a/packages/validator/src/types.ts b/packages/validation/src/types.ts similarity index 100% rename from packages/validator/src/types.ts rename to packages/validation/src/types.ts diff --git a/packages/validator/src/utils.ts b/packages/validation/src/utils.ts similarity index 100% rename from packages/validator/src/utils.ts rename to packages/validation/src/utils.ts diff --git a/packages/validator/src/validation.test.ts b/packages/validation/src/validation.test.ts similarity index 100% rename from packages/validator/src/validation.test.ts rename to packages/validation/src/validation.test.ts diff --git a/packages/validator/tsconfig.json b/packages/validation/tsconfig.json similarity index 100% rename from packages/validator/tsconfig.json rename to packages/validation/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 897955a..fa9b314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: packages/format: {} - packages/validator: {} + packages/validation: {} packages: From d291233295812df49b430deb09d9e6fb3ba3905b Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Mon, 10 Feb 2025 13:45:12 +0100 Subject: [PATCH 7/8] changeset --- .changeset/famous-humans-tell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/famous-humans-tell.md diff --git a/.changeset/famous-humans-tell.md b/.changeset/famous-humans-tell.md new file mode 100644 index 0000000..1a19f0e --- /dev/null +++ b/.changeset/famous-humans-tell.md @@ -0,0 +1,5 @@ +--- +"@obosbbl/validation": minor +--- + +initial release From 4321610b8196ed58aed76198bf4fcaf5fe6e9894 Mon Sep 17 00:00:00 2001 From: Alexander Bjerkan Date: Mon, 10 Feb 2025 14:57:51 +0100 Subject: [PATCH 8/8] be more strict when validating swedish mobile phone numbers Swedish mobile phone numbers only have 5 area codes, so we can validate these for further correctness --- packages/validation/src/se.ts | 3 ++- packages/validation/src/validation.test.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/validation/src/se.ts b/packages/validation/src/se.ts index a96f2d1..fdf41d0 100644 --- a/packages/validation/src/se.ts +++ b/packages/validation/src/se.ts @@ -52,7 +52,8 @@ export function validatePhoneNumber( } if (options.mobileOnly) { - const isMobileNumber = /^07\d{8}$/.test(value); + // Mobile numbers start with 07, followed by 0/2/3/6/9 and is 10 digits long + const isMobileNumber = /^07[02369]\d{7}$/.test(value); return isMobileNumber; } diff --git a/packages/validation/src/validation.test.ts b/packages/validation/src/validation.test.ts index 7e307b7..325512d 100644 --- a/packages/validation/src/validation.test.ts +++ b/packages/validation/src/validation.test.ts @@ -70,8 +70,21 @@ describe('se', () => { ['031-123 45', false, { allowFormatting: false }], // mobile only + // test all valid area codes (070, 072, 073, 076, 079) ['0701234567', true, { mobileOnly: true }], + ['0711234567', false, { mobileOnly: true }], + ['0721234567', true, { mobileOnly: true }], + ['0731234567', true, { mobileOnly: true }], + ['0741234567', false, { mobileOnly: true }], + ['0751234567', false, { mobileOnly: true }], + ['0761234567', true, { mobileOnly: true }], + ['0771234567', false, { mobileOnly: true }], + ['0781234567', false, { mobileOnly: true }], + ['0791234567', true, { mobileOnly: true }], + // landline ['031123456', false, { mobileOnly: true }], + // too long + ['07012345678', false, { mobileOnly: true }], ])('validatePhoneNumber(%s) -> %s', (input, expected, options) => { expect(se.validatePhoneNumber(input, options)).toBe(expected); });