@wisemen/address 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.env.test +1 -0
  2. package/README.md +83 -0
  3. package/dist/address-column.d.ts +2 -0
  4. package/dist/address-column.js +27 -0
  5. package/dist/address-column.js.map +1 -0
  6. package/dist/address-command.builder.d.ts +14 -0
  7. package/dist/address-command.builder.js +39 -0
  8. package/dist/address-command.builder.js.map +1 -0
  9. package/dist/address-command.d.ts +14 -0
  10. package/dist/address-command.js +104 -0
  11. package/dist/address-command.js.map +1 -0
  12. package/dist/address-response.d.ts +14 -0
  13. package/dist/address-response.js +72 -0
  14. package/dist/address-response.js.map +1 -0
  15. package/dist/address.builder.d.ts +16 -0
  16. package/dist/address.builder.js +47 -0
  17. package/dist/address.builder.js.map +1 -0
  18. package/dist/address.d.ts +12 -0
  19. package/dist/address.js +12 -0
  20. package/dist/address.js.map +1 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/is-address.validator.d.ts +11 -0
  25. package/dist/is-address.validator.js +52 -0
  26. package/dist/is-address.validator.js.map +1 -0
  27. package/dist/tests/address-column.integration.test.d.ts +1 -0
  28. package/dist/tests/address-column.integration.test.js +109 -0
  29. package/dist/tests/address-column.integration.test.js.map +1 -0
  30. package/dist/tests/is-address.validator.unit.test.d.ts +1 -0
  31. package/dist/tests/is-address.validator.unit.test.js +129 -0
  32. package/dist/tests/is-address.validator.unit.test.js.map +1 -0
  33. package/dist/tests/sql/address-test.entity.d.ts +5 -0
  34. package/dist/tests/sql/address-test.entity.js +28 -0
  35. package/dist/tests/sql/address-test.entity.js.map +1 -0
  36. package/dist/tests/sql/datasource.d.ts +2 -0
  37. package/dist/tests/sql/datasource.js +15 -0
  38. package/dist/tests/sql/datasource.js.map +1 -0
  39. package/eslint.config.js +17 -0
  40. package/lib/address-column.ts +31 -0
  41. package/lib/address-command.builder.ts +56 -0
  42. package/lib/address-command.ts +78 -0
  43. package/lib/address-response.ts +46 -0
  44. package/lib/address.builder.ts +68 -0
  45. package/lib/address.ts +13 -0
  46. package/lib/index.ts +7 -0
  47. package/lib/is-address.validator.ts +82 -0
  48. package/lib/tests/address-column.integration.test.ts +163 -0
  49. package/lib/tests/is-address.validator.unit.test.ts +144 -0
  50. package/lib/tests/sql/address-test.entity.ts +12 -0
  51. package/lib/tests/sql/datasource.ts +15 -0
  52. package/package.json +44 -0
  53. package/tsconfig.json +40 -0
@@ -0,0 +1,82 @@
1
+ import { Validate, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, ValidationOptions, IsObject, ValidateNested } from 'class-validator'
2
+ import { applyDecorators } from '@nestjs/common'
3
+ import { Type } from 'class-transformer'
4
+ import { AddressCommand } from './address-command.js'
5
+
6
+ export interface IsAddressValidationOptions extends ValidationOptions {
7
+ countryRequired?: boolean
8
+ cityRequired?: boolean
9
+ postalCodeRequired?: boolean
10
+ streetNameRequired?: boolean
11
+ streetNumberRequired?: boolean
12
+ unitRequired?: boolean
13
+ coordinatesRequired?: boolean
14
+ }
15
+
16
+ export function IsAddress (
17
+ validationOptions?: IsAddressValidationOptions
18
+ ): PropertyDecorator {
19
+ return applyDecorators(
20
+ ValidateNested(validationOptions),
21
+ Type(() => AddressCommand),
22
+ IsObject(validationOptions),
23
+ Validate(
24
+ IsAddressValidator,
25
+ [{
26
+ countryRequired: validationOptions?.countryRequired ?? false,
27
+ cityRequired: validationOptions?.cityRequired ?? false,
28
+ postalCodeRequired: validationOptions?.postalCodeRequired ?? false,
29
+ streetNameRequired: validationOptions?.streetNameRequired ?? false,
30
+ streetNumberRequired: validationOptions?.streetNumberRequired ?? false,
31
+ unitRequired: validationOptions?.unitRequired ?? false,
32
+ coordinatesRequired: validationOptions?.coordinatesRequired ?? false
33
+ }],
34
+ validationOptions
35
+ )
36
+ )
37
+ }
38
+
39
+ interface ValidatorConstraints {
40
+ countryRequired: boolean
41
+ cityRequired: boolean
42
+ postalCodeRequired: boolean
43
+ streetNameRequired: boolean
44
+ streetNumberRequired: boolean
45
+ unitRequired: boolean
46
+ coordinatesRequired: boolean
47
+ }
48
+
49
+ @ValidatorConstraint({ name: 'isAddress', async: false })
50
+ class IsAddressValidator implements ValidatorConstraintInterface {
51
+ validate (address: unknown, args: ValidationArguments): boolean {
52
+ if (!(address instanceof AddressCommand)) {
53
+ return false
54
+ }
55
+
56
+ const constraints = args.constraints[0] as ValidatorConstraints
57
+
58
+ return this.isAddressValid(address, constraints)
59
+ }
60
+
61
+ private isAddressValid (address: AddressCommand, constraints: ValidatorConstraints): boolean {
62
+ return (!constraints.countryRequired || address.country != null)
63
+ && (!constraints.cityRequired || address.city != null)
64
+ && (!constraints.postalCodeRequired || address.postalCode != null)
65
+ && (!constraints.streetNameRequired || address.streetName != null)
66
+ && (!constraints.streetNumberRequired || address.streetNumber != null)
67
+ && (!constraints.unitRequired || address.unit != null)
68
+ }
69
+
70
+ defaultMessage (args: ValidationArguments): string {
71
+ const constraints = args.constraints[0] as ValidatorConstraints
72
+ const requiredProperties: string[] = []
73
+
74
+ for (const key of Object.keys(constraints)) {
75
+ if (constraints[key] === true) {
76
+ requiredProperties.push(key)
77
+ }
78
+ }
79
+
80
+ return `${args.property}: missing ${requiredProperties.join(', ')}`
81
+ }
82
+ }
@@ -0,0 +1,163 @@
1
+ import { after, before, describe, it } from 'node:test'
2
+ import { expect } from 'expect'
3
+ import { Coordinates } from '@wisemen/coordinates'
4
+ import { AddressBuilder } from '../address.builder.js'
5
+ import { dataSource } from './sql/datasource.js'
6
+ import { AddressTest } from './sql/address-test.entity.js'
7
+
8
+ describe('AddressColumn', () => {
9
+ before(async () => {
10
+ await dataSource.initialize()
11
+ await dataSource.synchronize(true)
12
+ })
13
+
14
+ after(async () => {
15
+ await dataSource.destroy()
16
+ })
17
+
18
+ describe('serialization and deserialization', () => {
19
+ it('stores and retrieves a complete address with all fields', async () => {
20
+ const address = new AddressBuilder()
21
+ .withPlaceName('Main Office')
22
+ .withPlaceId('place123')
23
+ .withCountry('Belgium')
24
+ .withCity('Brussels')
25
+ .withPostalCode('1000')
26
+ .withStreetName('Grand Place')
27
+ .withStreetNumber('1')
28
+ .withUnit('A')
29
+ .withCoordinates(new Coordinates(50.8503, 4.3517))
30
+ .build()
31
+
32
+ await dataSource.manager.upsert(
33
+ AddressTest,
34
+ { id: 1, address },
35
+ { conflictPaths: { id: true } }
36
+ )
37
+
38
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 1 })
39
+
40
+ expect(test.address).not.toBeNull()
41
+ expect(test.address?.placeName).toBe('Main Office')
42
+ expect(test.address?.placeId).toBe('place123')
43
+ expect(test.address?.country).toBe('Belgium')
44
+ expect(test.address?.city).toBe('Brussels')
45
+ expect(test.address?.postalCode).toBe('1000')
46
+ expect(test.address?.streetName).toBe('Grand Place')
47
+ expect(test.address?.streetNumber).toBe('1')
48
+ expect(test.address?.unit).toBe('A')
49
+ expect(test.address?.coordinates?.latitude).toBe(50.8503)
50
+ expect(test.address?.coordinates?.longitude).toBe(4.3517)
51
+ })
52
+
53
+ it('stores and retrieves an address with partial fields', async () => {
54
+ const address = new AddressBuilder()
55
+ .withCountry('Belgium')
56
+ .withCity('Antwerp')
57
+ .withPostalCode('2000')
58
+ .build()
59
+
60
+ await dataSource.manager.upsert(
61
+ AddressTest,
62
+ { id: 2, address },
63
+ { conflictPaths: { id: true } }
64
+ )
65
+
66
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 2 })
67
+
68
+ expect(test.address).not.toBeNull()
69
+ expect(test.address?.country).toBe('Belgium')
70
+ expect(test.address?.city).toBe('Antwerp')
71
+ expect(test.address?.postalCode).toBe('2000')
72
+ expect(test.address?.streetName).toBeUndefined()
73
+ expect(test.address?.streetNumber).toBeUndefined()
74
+ expect(test.address?.unit).toBeUndefined()
75
+ expect(test.address?.coordinates).toBeUndefined()
76
+ })
77
+
78
+ it('stores and retrieves an address with coordinates only', async () => {
79
+ const address = new AddressBuilder()
80
+ .withCoordinates(new Coordinates(51.2194, 4.4025))
81
+ .build()
82
+
83
+ await dataSource.manager.upsert(
84
+ AddressTest,
85
+ { id: 3, address },
86
+ { conflictPaths: { id: true } }
87
+ )
88
+
89
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 3 })
90
+
91
+ expect(test.address).not.toBeNull()
92
+ expect(test.address?.coordinates?.latitude).toBe(51.2194)
93
+ expect(test.address?.coordinates?.longitude).toBe(4.4025)
94
+ expect(test.address?.country).toBeUndefined()
95
+ expect(test.address?.city).toBeUndefined()
96
+ })
97
+
98
+ it('stores and retrieves a null address', async () => {
99
+ await dataSource.manager.upsert(
100
+ AddressTest,
101
+ { id: 4, address: null },
102
+ { conflictPaths: { id: true } }
103
+ )
104
+
105
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 4 })
106
+
107
+ expect(test.address).toBeNull()
108
+ })
109
+
110
+ it('stores and retrieves an address with null coordinates', async () => {
111
+ const address = new AddressBuilder()
112
+ .withCountry('Netherlands')
113
+ .withCity('Amsterdam')
114
+ .withCoordinates(null)
115
+ .build()
116
+
117
+ await dataSource.manager.upsert(
118
+ AddressTest,
119
+ { id: 5, address },
120
+ { conflictPaths: { id: true } }
121
+ )
122
+
123
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 5 })
124
+
125
+ expect(test.address).not.toBeNull()
126
+ expect(test.address?.country).toBe('Netherlands')
127
+ expect(test.address?.city).toBe('Amsterdam')
128
+ expect(test.address?.coordinates).toBeUndefined()
129
+ })
130
+
131
+ it('updates an existing address', async () => {
132
+ const initialAddress = new AddressBuilder()
133
+ .withCountry('France')
134
+ .withCity('Paris')
135
+ .build()
136
+
137
+ await dataSource.manager.upsert(
138
+ AddressTest,
139
+ { id: 6, address: initialAddress },
140
+ { conflictPaths: { id: true } }
141
+ )
142
+
143
+ const updatedAddress = new AddressBuilder()
144
+ .withCountry('France')
145
+ .withCity('Lyon')
146
+ .withPostalCode('69000')
147
+ .build()
148
+
149
+ await dataSource.manager.upsert(
150
+ AddressTest,
151
+ { id: 6, address: updatedAddress },
152
+ { conflictPaths: { id: true } }
153
+ )
154
+
155
+ const test = await dataSource.manager.findOneByOrFail(AddressTest, { id: 6 })
156
+
157
+ expect(test.address).not.toBeNull()
158
+ expect(test.address?.country).toBe('France')
159
+ expect(test.address?.city).toBe('Lyon')
160
+ expect(test.address?.postalCode).toBe('69000')
161
+ })
162
+ })
163
+ })
@@ -0,0 +1,144 @@
1
+ import { describe, it } from 'node:test'
2
+ import { validate } from 'class-validator'
3
+ import { expect } from 'expect'
4
+ import { plainToInstance } from 'class-transformer'
5
+ import { AddressCommand } from '../address-command.js'
6
+ import { IsAddress } from '../is-address.validator.js'
7
+
8
+ class TestClass {
9
+ @IsAddress()
10
+ address: AddressCommand
11
+ }
12
+
13
+ class TestClassWithRequiredFields {
14
+ @IsAddress({
15
+ countryRequired: true,
16
+ cityRequired: true,
17
+ postalCodeRequired: true,
18
+ streetNameRequired: true
19
+ })
20
+ address: AddressCommand
21
+ }
22
+
23
+ describe('IsAddress decorator test', () => {
24
+ it('should pass validation when the address has all fields', async () => {
25
+ const testInstance = new TestClass()
26
+ const addressCommand = new AddressCommand()
27
+
28
+ addressCommand.country = 'Belgium'
29
+ addressCommand.city = 'Brussels'
30
+ addressCommand.postalCode = '1000'
31
+ addressCommand.streetName = 'Main Street'
32
+ addressCommand.streetNumber = '123'
33
+ addressCommand.unit = null
34
+ addressCommand.placeName = null
35
+ addressCommand.placeId = null
36
+ addressCommand.coordinates = null
37
+
38
+ testInstance.address = addressCommand
39
+
40
+ const errors = await validate(testInstance)
41
+
42
+ expect(errors.length).toBe(0)
43
+ })
44
+
45
+ it('should pass validation when the address has only some fields', async () => {
46
+ const testInstance = new TestClass()
47
+ const addressCommand = new AddressCommand()
48
+
49
+ addressCommand.country = 'Belgium'
50
+ addressCommand.city = null
51
+ addressCommand.postalCode = null
52
+ addressCommand.streetName = null
53
+ addressCommand.streetNumber = null
54
+ addressCommand.unit = null
55
+ addressCommand.placeName = null
56
+ addressCommand.placeId = null
57
+ addressCommand.coordinates = null
58
+
59
+ testInstance.address = addressCommand
60
+
61
+ const errors = await validate(testInstance)
62
+
63
+ expect(errors.length).toBe(0)
64
+ })
65
+
66
+ it('should fail validation when required fields are missing', async () => {
67
+ const testInstance = new TestClassWithRequiredFields()
68
+ const addressCommand = new AddressCommand()
69
+
70
+ addressCommand.country = null
71
+ addressCommand.city = null
72
+ addressCommand.postalCode = null
73
+ addressCommand.streetName = null
74
+ addressCommand.streetNumber = null
75
+ addressCommand.unit = null
76
+ addressCommand.placeName = null
77
+ addressCommand.placeId = null
78
+ addressCommand.coordinates = null
79
+
80
+ testInstance.address = addressCommand
81
+
82
+ const errors = await validate(testInstance)
83
+
84
+ expect(errors.length).toBe(1)
85
+ expect(errors[0].constraints?.isAddress).toContain('countryRequired')
86
+ expect(errors[0].constraints?.isAddress).toContain('cityRequired')
87
+ expect(errors[0].constraints?.isAddress).toContain('postalCodeRequired')
88
+ expect(errors[0].constraints?.isAddress).toContain('streetNameRequired')
89
+ })
90
+
91
+ it('should pass validation when all required fields are provided', async () => {
92
+ const testInstance = new TestClassWithRequiredFields()
93
+ const addressCommand = new AddressCommand()
94
+
95
+ addressCommand.country = 'Belgium'
96
+ addressCommand.city = 'Brussels'
97
+ addressCommand.postalCode = '1000'
98
+ addressCommand.streetName = 'Main Street'
99
+ addressCommand.streetNumber = null
100
+ addressCommand.unit = null
101
+ addressCommand.placeName = null
102
+ addressCommand.placeId = null
103
+ addressCommand.coordinates = null
104
+
105
+ testInstance.address = addressCommand
106
+
107
+ const errors = await validate(testInstance)
108
+
109
+ expect(errors.length).toBe(0)
110
+ })
111
+
112
+ it('should fail validation when some required fields are missing', async () => {
113
+ const testInstance = new TestClassWithRequiredFields()
114
+ const addressCommand = new AddressCommand()
115
+
116
+ addressCommand.country = 'Belgium'
117
+ addressCommand.city = 'Brussels'
118
+ addressCommand.postalCode = null
119
+ addressCommand.streetName = null
120
+ addressCommand.streetNumber = null
121
+ addressCommand.unit = null
122
+ addressCommand.placeName = null
123
+ addressCommand.placeId = null
124
+ addressCommand.coordinates = null
125
+
126
+ testInstance.address = addressCommand
127
+
128
+ const errors = await validate(testInstance)
129
+
130
+ expect(errors.length).toBe(1)
131
+ expect(errors[0].constraints?.isAddress).toContain('postalCodeRequired')
132
+ expect(errors[0].constraints?.isAddress).toContain('streetNameRequired')
133
+ })
134
+
135
+ it('should fail validation when the address is not an AddressCommand instance', async () => {
136
+ const testInstance = new TestClass()
137
+
138
+ testInstance.address = plainToInstance(AddressCommand, { country: 'Belgium' })
139
+
140
+ const errors = await validate(testInstance)
141
+
142
+ expect(errors.length).toBeGreaterThan(0)
143
+ })
144
+ })
@@ -0,0 +1,12 @@
1
+ import { Entity, PrimaryColumn } from 'typeorm'
2
+ import { AddressColumn } from '../../address-column.js'
3
+ import { Address } from '../../address.js'
4
+
5
+ @Entity()
6
+ export class AddressTest {
7
+ @PrimaryColumn()
8
+ id: number
9
+
10
+ @AddressColumn({nullable: true})
11
+ address: Address | null
12
+ }
@@ -0,0 +1,15 @@
1
+ import { DataSource } from 'typeorm'
2
+
3
+ export const dataSource = new DataSource({
4
+ name: 'default',
5
+ type: 'postgres',
6
+ url: process.env.DATABASE_URI,
7
+ ssl: false,
8
+ extra: { max: 50 },
9
+ logging: true,
10
+ synchronize: false,
11
+ migrationsRun: true,
12
+ entities: [
13
+ 'dist/**/*.entity.js'
14
+ ]
15
+ })
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@wisemen/address",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./lib/index.ts"
7
+ },
8
+ "imports": {
9
+ "#*": "./lib/*"
10
+ },
11
+ "peerDependencies": {
12
+ "@nestjs/common": "^11.1.12",
13
+ "@nestjs/swagger": "^11.2.5",
14
+ "@wisemen/coordinates": "0.0.17",
15
+ "@wisemen/validators": "^0.0.19",
16
+ "class-transformer": "^0.5.1",
17
+ "class-validator": "^0.14.3",
18
+ "typeorm": "^0.3.28"
19
+ },
20
+ "devDependencies": {
21
+ "@wisemen/eslint-config-nestjs": "^0.2.7",
22
+ "eslint": "9.39.2",
23
+ "eslint-plugin-import-typescript": "^0.0.4",
24
+ "eslint-plugin-unicorn": "62.0.0",
25
+ "expect": "30.2.0",
26
+ "typescript": "^5.9.3"
27
+ },
28
+ "author": "Kobe Kwanten",
29
+ "license": "GPL",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git@github.com:wisemen-digital/node-core.git"
33
+ },
34
+ "scripts": {
35
+ "prebuild": "rm -rf ./dist",
36
+ "clean": "rm -rf ./dist",
37
+ "build": "tsc",
38
+ "pretest": "npm run clean && npm run build",
39
+ "test": "node --env-file=.env.test --test ./**/*.test.js",
40
+ "lint": "eslint --cache",
41
+ "prerelease": "npm run build",
42
+ "release": "pnpx release-it"
43
+ }
44
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "declaration": true,
6
+ "target": "esnext",
7
+ "module": "NodeNext",
8
+ "types": [
9
+ "node",
10
+ ],
11
+ "moduleResolution": "nodenext",
12
+ "sourceMap": true,
13
+ "esModuleInterop": true,
14
+ "emitDecoratorMetadata": true,
15
+ "experimentalDecorators": true,
16
+ "resolveJsonModule": true,
17
+ "strictNullChecks": true,
18
+ "skipLibCheck": true
19
+ },
20
+ "include": [
21
+ "./index.ts",
22
+ "./lib/**/*.ts"
23
+ ],
24
+ "exclude": [
25
+ "./lib/test-utils.ts",
26
+ "./lib/**/*.spec.ts"
27
+ ],
28
+ "ts-node": {
29
+ "transpileOnly": true
30
+ },
31
+ "typedocOptions": {
32
+ "entryPoints": [
33
+ "./index.ts",
34
+ "./lib"
35
+ ],
36
+ "entryPointStrategy": "expand",
37
+ "out": "../../documentation/client"
38
+ }
39
+ }
40
+