Skip to content

Commit 66676fe

Browse files
authored
add validation package (#17)
1 parent 47e13b3 commit 66676fe

File tree

11 files changed

+445
-0
lines changed

11 files changed

+445
-0
lines changed

.changeset/famous-humans-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@obosbbl/validation": minor
3+
---
4+
5+
initial release

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This a monorepo of OBOS' open source frontend modules.
66
## Packages
77

88
* [format](./packages/format)
9+
* [validation](./packages/validation)
910

1011
## Contributing
1112

packages/validation/README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# @obosbbl/validation
2+
3+
[![NPM Version](https://img.shields.io/npm/v/%40obosbbl%2Fvalidation)](https://www.npmjs.com/package/@obosbbl/validation)
4+
5+
6+
A collection of validation methods for both 🇳🇴 and 🇸🇪 with zero dependencies.
7+
8+
## Install
9+
10+
```sh
11+
# npm
12+
npm install @obosbbl/validation
13+
14+
# pnpm
15+
pnpm add @obosbbl/validation
16+
```
17+
18+
## Usage
19+
20+
The package has two entrypoints, one for `no` and one for `se`. That allows you to import for only the locale you need.
21+
22+
23+
```js
24+
// 🇳🇴 example
25+
import { validateOrganizationNumber } from '@obosbbl/validation/no';
26+
validateOrganizationNumber('937052766') // => true
27+
28+
validateOrganizationNumber('000') // => false
29+
30+
// 🇸🇪 example
31+
import { validateOrganizationNumer } from '@obosbbl/validation/se';
32+
validateOrganizationNumber('5592221054') // => true
33+
34+
validateOrganizationNumber('000') // => false
35+
```
36+
37+
## Strictness and formatting characters
38+
39+
The methods are "strict" by default, meaning no formatting characters in the input is allowed.
40+
This is preferrable, for instance when doing server-side validation, where the input is often expected to be a "clean" value.
41+
42+
If you want to allow formatting characters in the input, you can pass `allowFormatting: true` in the options object to the method.
43+
Note that this currently allows any formatting characters, not just the just the "expected" ones for the input type.
44+
45+
46+
```js
47+
import { validateOrganizationNumber } from '@obosbbl/validation/no';
48+
49+
validateOrganizationNumber('937052766') // true
50+
// formatting characters disallowed by default
51+
validateOrganizationNumber('937 052 766') // false;
52+
// allow formatting characters
53+
validateOrganizationNumber('937 052 766', { allowFormatting: true }) // true;
54+
```
55+
56+
## Methods
57+
58+
* validatePostalCode
59+
* validatePhoneNumber
60+
* supports mobileOnly option
61+
* validateOrganizationNumber
62+
* 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.
63+
64+
## Example usage with Zod
65+
66+
```js
67+
import { z } from 'zod';
68+
import { validatePhoneNumber } from '@obosbbl/validation/no';
69+
70+
const mobileOnlySchema = z.object({
71+
name: z.string(),
72+
phoneNumber: z
73+
.string()
74+
.refine(
75+
(val) => validatePhoneNumber(val, { mobileOnly: true }),
76+
'Telefonnummeret er ikke et gyldig mobilnummer',
77+
),
78+
});
79+
80+
const validData = {
81+
name: 'Kari Nordmann',
82+
phoneNumber: '92345678',
83+
};
84+
85+
mobileOnlySchema.parse(validData); // => { name: 'Kari Nordmann', phoneNumber: '92345678' }
86+
87+
const invalidData = {
88+
name: 'Ola Nordmann',
89+
phoneNumber: '22865500',
90+
}
91+
92+
mobileOnlySchema.parse(invalidData); // => throws ZodError
93+
```

packages/validation/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@obosbbl/validation",
3+
"version": "0.0.0",
4+
"description": "A collection of validation methods for OBOS",
5+
"repository": {
6+
"url": "https://github.com/code-obos/public-frontend-modules"
7+
},
8+
"license": "MIT",
9+
"sideEffects": false,
10+
"type": "module",
11+
"exports": {
12+
"./no": {
13+
"types": "./dist/no.d.mts",
14+
"default": "./dist/no.mjs"
15+
},
16+
"./se": {
17+
"types": "./dist/se.d.mts",
18+
"default": "./dist/se.mjs"
19+
}
20+
},
21+
"files": [
22+
"dist"
23+
],
24+
"scripts": {
25+
"build": "bunchee"
26+
}
27+
}

packages/validation/src/no.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { ValidatorOptions } from './types';
2+
import { mod11, stripFormatting } from './utils';
3+
4+
type PostalCodeOptions = ValidatorOptions;
5+
6+
/**
7+
* Validates that the input value is a Norwegian postal (zip) code.
8+
* @example
9+
* ```
10+
* validatePostalCode('0000') // => true
11+
* ```
12+
*/
13+
export function validatePostalCode(
14+
value: string,
15+
options: PostalCodeOptions = {},
16+
): boolean {
17+
if (options.allowFormatting) {
18+
// biome-ignore lint/style/noParameterAssign:
19+
value = stripFormatting(value);
20+
}
21+
22+
return /^\d{4}$/.test(value);
23+
}
24+
25+
type PhoneNumberOptions = ValidatorOptions & {
26+
/**
27+
* Whether it should be a mobile number
28+
* @default false
29+
*/
30+
mobileOnly?: boolean;
31+
};
32+
33+
/**
34+
* Validates that the input value is a Norwegian phone number.
35+
*
36+
* Supports mobile only validation.
37+
* @example
38+
* ```
39+
* validatePhoneNumber('00000000') // => true
40+
* validatePhoneNumber('90000000', { mobileOnly: true }) // => true
41+
* ```
42+
*/
43+
export function validatePhoneNumber(
44+
value: string,
45+
options: PhoneNumberOptions = {},
46+
): boolean {
47+
if (options.allowFormatting) {
48+
// biome-ignore lint/style/noParameterAssign:
49+
value = stripFormatting(value);
50+
}
51+
52+
const isPhoneNumber = /^\d{8}$/.test(value);
53+
54+
if (options.mobileOnly) {
55+
// Norwegian mobile phone numbers start with 4 or 9
56+
// See https://nkom.no/telefoni-og-telefonnummer/telefonnummer-og-den-norske-nummerplan/alle-nummerserier-for-norske-telefonnumre
57+
return isPhoneNumber && ['4', '9'].includes(value.charAt(0));
58+
}
59+
60+
return isPhoneNumber;
61+
}
62+
63+
type OrganizationNumberOptions = ValidatorOptions;
64+
65+
/**
66+
* Validates that the input value is a {@link https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/ Norwegian organization number}.
67+
* @example
68+
* ```
69+
* validateOrganizationNumber('000000000') // => true
70+
* ```
71+
*/
72+
export function validateOrganizationNumber(
73+
value: string,
74+
options: PhoneNumberOptions = {},
75+
): boolean {
76+
if (options.allowFormatting) {
77+
// biome-ignore lint/style/noParameterAssign:
78+
value = stripFormatting(value);
79+
}
80+
81+
/** References:
82+
* https://www.brreg.no/om-oss/registrene-vare/om-enhetsregisteret/organisasjonsnummeret/
83+
* https://no.wikipedia.org/wiki/Organisasjonsnummer
84+
*/
85+
return mod11(value, [3, 2, 7, 6, 5, 4, 3, 2]);
86+
}

packages/validation/src/se.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { ValidatorOptions } from './types';
2+
import { stripFormatting } from './utils';
3+
4+
type PostalCodeOptions = ValidatorOptions;
5+
6+
/**
7+
* Validates that the input value is a Swedish postal (zip) code.
8+
* @example
9+
* ```
10+
* validatePostalCode('00000') // => true
11+
* ```
12+
*/
13+
export function validatePostalCode(
14+
value: string,
15+
options: PostalCodeOptions = {},
16+
): boolean {
17+
if (options.allowFormatting) {
18+
// biome-ignore lint/style/noParameterAssign:
19+
value = stripFormatting(value);
20+
}
21+
22+
return /^\d{5}$/.test(value);
23+
}
24+
25+
type PhoneNumberOptions = ValidatorOptions & {
26+
/**
27+
* Whether it should be a mobile number
28+
* @default false
29+
*/
30+
mobileOnly?: boolean;
31+
};
32+
33+
/**
34+
* Validates that the input value is a Swedish phone number.
35+
*
36+
* Supports mobile only validation.
37+
* @example
38+
* ```
39+
* validatePhoneNumber('00000000') // => true
40+
* validatePhoneNumber('000000000') // => true
41+
* validatePhoneNumber('0000000000') // => true
42+
* validatePhoneNumber('0700000000', { mobileOnly: true }) // => true
43+
* ```
44+
*/
45+
export function validatePhoneNumber(
46+
value: string,
47+
options: PhoneNumberOptions = {},
48+
): boolean {
49+
if (options.allowFormatting) {
50+
// biome-ignore lint/style/noParameterAssign:
51+
value = stripFormatting(value);
52+
}
53+
54+
if (options.mobileOnly) {
55+
// Mobile numbers start with 07, followed by 0/2/3/6/9 and is 10 digits long
56+
const isMobileNumber = /^07[02369]\d{7}$/.test(value);
57+
return isMobileNumber;
58+
}
59+
60+
const isPhoneNumber = /^0\d{7,9}$/.test(value);
61+
62+
return isPhoneNumber;
63+
}
64+
65+
type OrganizationNumberOptions = ValidatorOptions;
66+
67+
/**
68+
* Validates that the input value is a {@link https://www.skatteverket.se/foretagochorganisationer/foretagare/startaochregistrera/organisationsnummer.4.361dc8c15312eff6fd235d1.html Swedish organization number}.
69+
* @example
70+
* ```
71+
* validateOrganizationNumber('000000000') // => true
72+
* ```
73+
*/
74+
export function validateOrganizationNumber(
75+
value: string,
76+
options: OrganizationNumberOptions = {},
77+
): boolean {
78+
// TODO: Implement checksum validation. For now it only checks the number of digits.
79+
if (options.allowFormatting) {
80+
// biome-ignore lint/style/noParameterAssign:
81+
value = stripFormatting(value);
82+
}
83+
84+
return /^\d{10}$/.test(value);
85+
}

packages/validation/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type ValidatorOptions = {
2+
/**
3+
* Allow formatting characters
4+
* @default false
5+
*/
6+
allowFormatting?: boolean;
7+
};

packages/validation/src/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/** Strip all formatting, leaving only numbers and letters */
2+
export function stripFormatting(value: string): string {
3+
return value.replace(/[^a-zA-Z0-9]/g, '');
4+
}
5+
6+
/**
7+
* Used to validate Norwegian bank account numbers, organization numbers and national identity numbers.
8+
* See https://no.wikipedia.org/wiki/MOD11
9+
*/
10+
export function mod11(value: string, weights: number[]): boolean {
11+
// Since the control digit is a single value, the lengths should be equal
12+
if (weights.length + 1 !== value.length) {
13+
return false;
14+
}
15+
16+
let sum = 0;
17+
weights.forEach((weight, index) => {
18+
sum += weight * Number(value[index]);
19+
});
20+
21+
let controlNumber = 11 - (sum % 11);
22+
23+
if (controlNumber === 11) {
24+
controlNumber = 0;
25+
}
26+
27+
return controlNumber === Number(value[value.length - 1]);
28+
}

0 commit comments

Comments
 (0)