Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-humans-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@obosbbl/validation": minor
---

initial release
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This a monorepo of OBOS' open source frontend modules.
## Packages

* [format](./packages/format)
* [validation](./packages/validation)

## Contributing

Expand Down
93 changes: 93 additions & 0 deletions packages/validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# @obosbbl/validation

[![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.

## 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 { validateOrganizationNumber } from '@obosbbl/validation/no';
validateOrganizationNumber('937052766') // => true

validateOrganizationNumber('000') // => false

// 🇸🇪 example
import { validateOrganizationNumer } from '@obosbbl/validation/se';
validateOrganizationNumber('5592221054') // => true

validateOrganizationNumber('000') // => false
```

## 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.


```js
import { validateOrganizationNumber } from '@obosbbl/validation/no';

validateOrganizationNumber('937052766') // true
// formatting characters disallowed by default
validateOrganizationNumber('937 052 766') // false;
// allow formatting characters
validateOrganizationNumber('937 052 766', { allowFormatting: true }) // true;
```

## Methods

* validatePostalCode
* validatePhoneNumber
* 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
```
27 changes: 27 additions & 0 deletions packages/validation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"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"
}
}
86 changes: 86 additions & 0 deletions packages/validation/src/no.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { ValidatorOptions } from './types';
import { mod11, stripFormatting } from './utils';

type PostalCodeOptions = ValidatorOptions;

/**
* 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.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
value = stripFormatting(value);
}

return /^\d{4}$/.test(value);
}

type PhoneNumberOptions = ValidatorOptions & {
/**
* Whether it should be a mobile number
* @default false
*/
mobileOnly?: 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.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
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;
}

type OrganizationNumberOptions = ValidatorOptions;

/**
* 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,
options: PhoneNumberOptions = {},
): boolean {
if (options.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
value = stripFormatting(value);
}

/** 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]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a-black-screen-with-green-numbers-on-it-that-says-4-8-15-16-23-42

}
85 changes: 85 additions & 0 deletions packages/validation/src/se.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { ValidatorOptions } from './types';
import { stripFormatting } from './utils';

type PostalCodeOptions = ValidatorOptions;

/**
* 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.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
value = stripFormatting(value);
}

return /^\d{5}$/.test(value);
}

type PhoneNumberOptions = ValidatorOptions & {
/**
* Whether it should be a mobile number
* @default false
*/
mobileOnly?: 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.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
value = stripFormatting(value);
}

if (options.mobileOnly) {
// 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;
}

const isPhoneNumber = /^0\d{7,9}$/.test(value);

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: OrganizationNumberOptions = {},
): boolean {
// TODO: Implement checksum validation. For now it only checks the number of digits.
if (options.allowFormatting) {
// biome-ignore lint/style/noParameterAssign:
value = stripFormatting(value);
}

return /^\d{10}$/.test(value);
}
7 changes: 7 additions & 0 deletions packages/validation/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ValidatorOptions = {
/**
* Allow formatting characters
* @default false
*/
allowFormatting?: boolean;
};
28 changes: 28 additions & 0 deletions packages/validation/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Krem eksempel på en liten reduce? 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Det skal sies at var opprinnelig en reduce faktisk, men reduce er en av de tingene jeg har sluttet å bruke. 9/10 ganger er ikke-reduce varianten mer lesbar, imo.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe, må nok si meg uenig her ja 😆
16 år etter at reduce ble introdusert til js så er ikkje det noe "magisk" eller uleselige greier lenger 😉

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]);
}
Loading