@haskou/value-objects 1.0.3 → 1.1.0

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Value Objects
4
4
 
5
- A TypeScript dependency-less library for creating safe, immutable,
5
+ A TypeScript lightweight library for creating safe, immutable,
6
6
  and validated **Value Objects**. Perfect for applications that require **Domain-Driven Design (DDD)** and **type safety**.
7
7
 
8
8
  ## 🚀 Quick Start
@@ -58,6 +58,10 @@ your application.
58
58
  - **`Integer`** - Whole numbers
59
59
  - **`PositiveNumber`** - Positive numbers (> 0)
60
60
 
61
+ ### 🆔 Identifiers
62
+ - **`ShortId`** - MongoDB ObjectId strings (24‑character hex)
63
+ - **`UUID`** - Universally Unique Identifier (v4)
64
+
61
65
  ### ✨ Specialized
62
66
  - **`Email`** - Email addresses with automatic validation
63
67
  - **`Color`** - Hex colors (#FF0000)
@@ -79,7 +83,12 @@ your application.
79
83
  - **`Longitude`** - Longitude (-180 to 180)
80
84
  - **`Coordinates`** - Coordinate pairs
81
85
 
82
- ### 📝 Other
86
+ ### Hashes
87
+ - **`MD5Hash`** - MD5 hashes with helpers
88
+ - **`SHA256Hash`** - SHA‑256 hashes with helpers
89
+ - **`SHA512Hash`** - SHA‑512 hashes with helpers
90
+
91
+ ### �📝 Other
83
92
  - **`Enum`** - Base class for typed enumerations
84
93
 
85
94
  ## 💡 Basic Examples
@@ -111,6 +120,16 @@ const coords = new Coordinates(latitude, longitude);
111
120
  const hour = new Hour('09:30');
112
121
  const year = new Year(2024);
113
122
  console.log(year.isLeapYear()); // true
123
+
124
+ // IDs
125
+ const id = ShortId.generate(); // 69ad70897364ee0d1406b1d0
126
+ const uuid = UUID.generate(); // 3fd4c04a-8e73-4e10-aef3-f491b32ec538
127
+
128
+ // Hashes
129
+ const md5 = MD5Hash.from('hello'); // 5d41402abc4b2a76b9719d911017c592
130
+ const sha256 = SHA256Hash.from('hello'); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
131
+ const sha512 = SHA512Hash.from('hello'); // 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043
132
+ console.log(md5.toBase64());
114
133
  ```
115
134
 
116
135
  ## 📚 Technical Documentation
@@ -13,6 +13,7 @@ Comprehensive technical documentation for the Value Objects library.
13
13
  - [Year Value Objects](#year-value-objects)
14
14
  - [Color Value Objects](#color-value-objects)
15
15
  - [Email Value Objects](#email-value-objects)
16
+ - [ID Value Objects](#id-value-objects)
16
17
  - [Hour Value Objects](#hour-value-objects)
17
18
  - [Time Value Objects](#time-value-objects)
18
19
  - [Enum Value Objects](#enum-value-objects)
@@ -471,6 +472,171 @@ const validEmails = [
471
472
  ];
472
473
  ```
473
474
 
475
+ ### ID Value Objects
476
+
477
+ #### ShortId
478
+
479
+ Represents immutable MongoDB ObjectId values with automatic generation and validation.
480
+
481
+ ```typescript
482
+ class ShortId extends ValueObject<string> {
483
+ public static generate(): ShortId;
484
+ constructor(value: string | StringValueObject);
485
+ }
486
+ ```
487
+
488
+ **Example:**
489
+ ```typescript
490
+ // Generate a new ObjectId
491
+ const id = ShortId.generate();
492
+ console.log(id.toString()); // '507f1f77bcf86cd799439011' (24-character hex string)
493
+
494
+ // Create from existing ObjectId string
495
+ const existingId = new ShortId('507f1f77bcf86cd799439011');
496
+
497
+ // From StringValueObject
498
+ const idString = new StringValueObject('507f1f77bcf86cd799439011');
499
+ const idFromString = new ShortId(idString);
500
+
501
+ // String representation
502
+ console.log(existingId.toString()); // '507f1f77bcf86cd799439011'
503
+ console.log(existingId.valueOf()); // '507f1f77bcf86cd799439011'
504
+
505
+ // Equality comparison
506
+ const id1 = new ShortId('507f1f77bcf86cd799439011');
507
+ const id2 = new ShortId('507f1f77bcf86cd799439011');
508
+ console.log(id1.isEqual(id2)); // true
509
+
510
+ // Validation
511
+ try {
512
+ new ShortId('invalid-id'); // Throws InvalidFormatError
513
+ new ShortId('short'); // Throws InvalidLengthError
514
+ } catch (error) {
515
+ console.error('Invalid ObjectId format');
516
+ }
517
+ ```
518
+
519
+ #### UUID
520
+
521
+ Represents immutable UUID (Universally Unique Identifier) values with automatic generation and validation.
522
+
523
+ ```typescript
524
+ class UUID extends ValueObject<string> {
525
+ public static generate(): UUID;
526
+ constructor(value: string | StringValueObject);
527
+ }
528
+ ```
529
+
530
+ **Example:**
531
+ ```typescript
532
+ // Generate a new UUID v4
533
+ const uuid = UUID.generate();
534
+ console.log(uuid.toString()); // '550e8400-e29b-41d4-a716-446655440000' (36-character string)
535
+
536
+ // Create from existing UUID string
537
+ const existingUuid = new UUID('550e8400-e29b-41d4-a716-446655440000');
538
+
539
+ // From StringValueObject
540
+ const uuidString = new StringValueObject('550e8400-e29b-41d4-a716-446655440000');
541
+ const uuidFromString = new UUID(uuidString);
542
+
543
+ // String representation
544
+ console.log(existingUuid.toString()); // '550e8400-e29b-41d4-a716-446655440000'
545
+ console.log(existingUuid.valueOf()); // '550e8400-e29b-41d4-a716-446655440000'
546
+
547
+ // Equality comparison
548
+ const uuid1 = new UUID('550e8400-e29b-41d4-a716-446655440000');
549
+ const uuid2 = new UUID('550e8400-e29b-41d4-a716-446655440000');
550
+ console.log(uuid1.isEqual(uuid2)); // true
551
+
552
+ // Validation
553
+ try {
554
+ new UUID('invalid-uuid'); // Throws InvalidFormatError
555
+ new UUID('short-uuid'); // Throws InvalidLengthError
556
+ } catch (error) {
557
+ console.error('Invalid UUID format');
558
+ }
559
+ ```
560
+
561
+ ### Hash Value Objects
562
+
563
+ #### MD5Hash
564
+ Represents immutable MD5 hashes with validation and utility methods.
565
+
566
+ ```typescript
567
+ class MD5Hash extends Hash {
568
+ public static isValid(hash: string | StringValueObject): boolean;
569
+ public static from(buffer: Buffer | string | StringValueObject): MD5Hash;
570
+ constructor(source: string | StringValueObject);
571
+ }
572
+ ```
573
+
574
+ **Example:**
575
+ ```typescript
576
+ // Compute an MD5 hash from a string or buffer
577
+ const md5 = MD5Hash.from('hello');
578
+ console.log(md5.toString()); // '5d41402abc4b2a76b9719d911017c592'
579
+ console.log(md5.toBase64()); // 'XUEQKvLG6avlckeQEAXGSw=='
580
+
581
+ // Create from existing hash string
582
+ const existing = new MD5Hash('5d41402abc4b2a76b9719d911017c592');
583
+
584
+ // Validation
585
+ try {
586
+ new MD5Hash('invalid'); // Throws InvalidHashError
587
+ } catch (err) {
588
+ console.error('Invalid MD5 hash');
589
+ }
590
+ ```
591
+
592
+ #### SHA256Hash
593
+ Represents immutable SHA‑256 hashes with validation and utility methods.
594
+
595
+ ```typescript
596
+ class SHA256Hash extends ValueObject<string> {
597
+ public static isValid(hash: string | StringValueObject): boolean;
598
+ public static from(buffer: Buffer | string | StringValueObject): SHA256Hash;
599
+ constructor(source: string | StringValueObject);
600
+ }
601
+ ```
602
+
603
+ **Example:**
604
+ ```typescript
605
+ const sha256 = SHA256Hash.from('hello');
606
+ console.log(sha256.toString().length); // 64
607
+ console.log(sha256.toBase64());
608
+
609
+ try {
610
+ new SHA256Hash('abc'); // Throws InvalidHashError
611
+ } catch (err) {
612
+ console.error('Invalid SHA256 hash');
613
+ }
614
+ ```
615
+
616
+ #### SHA512Hash
617
+ Represents immutable SHA‑512 hashes with validation and utility methods.
618
+
619
+ ```typescript
620
+ class SHA512Hash extends ValueObject<string> {
621
+ public static isValid(hash: string | StringValueObject): boolean;
622
+ public static from(buffer: Buffer | string | StringValueObject): SHA512Hash;
623
+ constructor(source: string | StringValueObject);
624
+ }
625
+ ```
626
+
627
+ **Example:**
628
+ ```typescript
629
+ const sha512 = SHA512Hash.from('hello');
630
+ console.log(sha512.toString().length); // 128
631
+ console.log(sha512.toBase64());
632
+
633
+ try {
634
+ new SHA512Hash('abc'); // Throws InvalidHashError
635
+ } catch (err) {
636
+ console.error('Invalid SHA512 hash');
637
+ }
638
+ ```
639
+
474
640
  ### Hour Value Objects
475
641
 
476
642
  #### Hour
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haskou/value-objects",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "main": "src/index.ts",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,6 +28,7 @@
28
28
  "devDependencies": {
29
29
  "@types/jest": "^30.0.0",
30
30
  "@types/node": "^24.0.13",
31
+ "@types/uuid": "8",
31
32
  "@typescript-eslint/eslint-plugin": "6.20.0",
32
33
  "@typescript-eslint/parser": "6.20.0",
33
34
  "eslint": "8.57.0",
@@ -45,5 +46,9 @@
45
46
  "ts-jest": "^29.4.0",
46
47
  "ts-node": "^10.9.2",
47
48
  "typescript": "^5.8.3"
49
+ },
50
+ "dependencies": {
51
+ "bson-objectid": "^2.0.4",
52
+ "uuid": "8"
48
53
  }
49
54
  }
@@ -0,0 +1,7 @@
1
+ import { DomainError } from '../errors/DomainError';
2
+
3
+ export class InvalidFormatError extends DomainError {
4
+ constructor(value?: string) {
5
+ super(`Invalid Format Error with value: '${value}'`);
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import { DomainError } from './DomainError';
2
+
3
+ export class InvalidHashError extends DomainError {
4
+ constructor(type: string, value: string) {
5
+ super(`Invalid ${type} hash: ${value}`);
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ import { DomainError } from './DomainError';
2
+
3
+ export class InvalidLengthError extends DomainError {
4
+ constructor(value: unknown, length: number) {
5
+ super(
6
+ `Invalid Length Error with value: '${value}' expected length: '${length}'`,
7
+ );
8
+ }
9
+ }
@@ -0,0 +1,7 @@
1
+ import { DomainError } from './DomainError';
2
+
3
+ export class InvalidValueError extends DomainError {
4
+ constructor(message: string = 'Invalid Value Error', value?: unknown) {
5
+ super(`${message} with value: '${value}'`);
6
+ }
7
+ }
@@ -4,6 +4,9 @@ export * from './InvalidColorError';
4
4
  export * from './InvalidDayError';
5
5
  export * from './InvalidDayFormatError';
6
6
  export * from './InvalidHourError';
7
+ export * from './InvalidEmailError';
8
+ export * from './InvalidFormatError';
9
+ export * from './InvalidHashError';
7
10
  export * from './InvalidIntegerError';
8
11
  export * from './InvalidLatitudeError';
9
12
  export * from './InvalidLongitudeError';
@@ -1,3 +1 @@
1
1
  export * from './Assert';
2
- export * from '../value-objects/NullObject';
3
- export * from '../value-objects/ValueObject';
@@ -0,0 +1,19 @@
1
+ import { NullObject } from '../NullObject';
2
+ import { StringValueObject } from '../StringValueObject';
3
+ import { ValueObject } from '../ValueObject';
4
+
5
+ export abstract class Hash extends ValueObject<string> {
6
+ constructor(source: string | StringValueObject) {
7
+ super(source?.valueOf());
8
+
9
+ if (NullObject.isNullObject(this)) {
10
+ return this;
11
+ }
12
+ }
13
+
14
+ public toBase64(): StringValueObject {
15
+ return new StringValueObject(
16
+ Buffer.from(this.valueOf(), 'hex').toString('base64'),
17
+ );
18
+ }
19
+ }
@@ -0,0 +1,32 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { InvalidHashError } from '../../errors/InvalidHashError';
4
+ import { assert } from '../../patterns';
5
+ import { NullObject } from '../NullObject';
6
+ import { StringValueObject } from '../StringValueObject';
7
+ import { Hash } from './Hash';
8
+
9
+ export class MD5Hash extends Hash {
10
+ public static isValid(hash: string | StringValueObject): boolean {
11
+ return !!hash.valueOf().match(/^[a-f0-9]{32}$/);
12
+ }
13
+
14
+ public static from(buffer: Buffer | string | StringValueObject): MD5Hash {
15
+ return new MD5Hash(
16
+ crypto.createHash('md5').update(buffer.valueOf()).digest('hex'),
17
+ );
18
+ }
19
+
20
+ constructor(source: string | StringValueObject) {
21
+ super(source?.valueOf());
22
+
23
+ if (NullObject.isNullObject(this)) {
24
+ return this;
25
+ }
26
+
27
+ assert(
28
+ MD5Hash.isValid(this.valueOf()),
29
+ new InvalidHashError('MD5', source.valueOf()),
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ import { InvalidHashError } from '../../errors/InvalidHashError';
4
+ import { assert } from '../../patterns';
5
+ import { NullObject } from '../NullObject';
6
+ import { StringValueObject } from '../StringValueObject';
7
+ import { ValueObject } from '../ValueObject';
8
+
9
+ export class SHA256Hash extends ValueObject<string> {
10
+ public static isValid(hash: string | StringValueObject): boolean {
11
+ return !!hash.valueOf().match(/^[a-f0-9]{64}$/i);
12
+ }
13
+
14
+ public static from(buffer: Buffer | string | StringValueObject): SHA256Hash {
15
+ return new SHA256Hash(
16
+ createHash('sha256').update(buffer.valueOf()).digest('hex'),
17
+ );
18
+ }
19
+
20
+ constructor(source: string | StringValueObject) {
21
+ super(source?.valueOf());
22
+
23
+ if (NullObject.isNullObject(this)) {
24
+ return this;
25
+ }
26
+
27
+ assert(
28
+ SHA256Hash.isValid(this.valueOf()),
29
+ new InvalidHashError('SHA256', source.valueOf()),
30
+ );
31
+ }
32
+
33
+ public toBase64(): StringValueObject {
34
+ return new StringValueObject(
35
+ Buffer.from(this.valueOf(), 'hex').toString('base64'),
36
+ );
37
+ }
38
+ }
@@ -0,0 +1,38 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ import { InvalidHashError } from '../../errors/InvalidHashError';
4
+ import { assert } from '../../patterns';
5
+ import { NullObject } from '../NullObject';
6
+ import { StringValueObject } from '../StringValueObject';
7
+ import { ValueObject } from '../ValueObject';
8
+
9
+ export class SHA512Hash extends ValueObject<string> {
10
+ public static isValid(hash: string | StringValueObject): boolean {
11
+ return !!hash.valueOf().match(/^[a-f0-9]{128}$/i);
12
+ }
13
+
14
+ public static from(buffer: Buffer | string | StringValueObject): SHA512Hash {
15
+ return new SHA512Hash(
16
+ createHash('sha512').update(buffer.valueOf()).digest('hex'),
17
+ );
18
+ }
19
+
20
+ constructor(source: string | StringValueObject) {
21
+ super(source?.valueOf());
22
+
23
+ if (NullObject.isNullObject(this)) {
24
+ return this;
25
+ }
26
+
27
+ assert(
28
+ SHA512Hash.isValid(this.valueOf()),
29
+ new InvalidHashError('SHA512', source.valueOf()),
30
+ );
31
+ }
32
+
33
+ public toBase64(): StringValueObject {
34
+ return new StringValueObject(
35
+ Buffer.from(this.valueOf(), 'hex').toString('base64'),
36
+ );
37
+ }
38
+ }
@@ -0,0 +1,4 @@
1
+ export * from './Hash';
2
+ export * from './MD5Hash';
3
+ export * from './SHA256Hash';
4
+ export * from './SHA512Hash';
@@ -0,0 +1,34 @@
1
+ import ObjectId from 'bson-objectid';
2
+
3
+ import { InvalidFormatError } from '../../errors/InvalidFormatError';
4
+ import { InvalidLengthError } from '../../errors/InvalidLengthError';
5
+ import { assert } from '../../patterns';
6
+ import { NullObject } from '../NullObject';
7
+ import { StringValueObject } from '../StringValueObject';
8
+ import { ValueObject } from '../ValueObject';
9
+
10
+ export class ShortId extends ValueObject<string> {
11
+ private static readonly LENGTH = 24;
12
+ private static readonly PATTERN = new RegExp(`[a-zA-Z0-9]{${this.LENGTH}}$`);
13
+
14
+ public static generate(): ShortId {
15
+ return new ShortId(ObjectId().toHexString());
16
+ }
17
+
18
+ constructor(value: string | StringValueObject) {
19
+ super(value?.valueOf());
20
+
21
+ if (NullObject.isNullObject(this)) {
22
+ return this;
23
+ }
24
+ this.ensureIsShortId(this.value);
25
+ }
26
+
27
+ private ensureIsShortId(value: string): void {
28
+ assert(
29
+ value.length === ShortId.LENGTH,
30
+ new InvalidLengthError(this.value, ShortId.LENGTH),
31
+ );
32
+ assert(ShortId.PATTERN.test(value), new InvalidFormatError(this.value));
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ import { v4 } from 'uuid';
2
+
3
+ import { InvalidFormatError } from '../../errors/InvalidFormatError';
4
+ import { InvalidLengthError } from '../../errors/InvalidLengthError';
5
+ import { assert } from '../../patterns';
6
+ import { NullObject } from '../NullObject';
7
+ import { StringValueObject } from '../StringValueObject';
8
+ import { ValueObject } from '../ValueObject';
9
+
10
+ export class UUID extends ValueObject<string> {
11
+ private static readonly LENGTH = 36;
12
+ private static readonly PATTERN = new RegExp(`^[a-z0-9-]{${this.LENGTH}}$`);
13
+
14
+ public static generate(): UUID {
15
+ return new UUID(v4());
16
+ }
17
+
18
+ constructor(value: string | StringValueObject) {
19
+ super(value?.valueOf());
20
+
21
+ if (NullObject.isNullObject(this)) {
22
+ return this;
23
+ }
24
+ this.ensureIsUUID(this.value);
25
+ }
26
+
27
+ private ensureIsUUID(value: string): void {
28
+ assert(
29
+ value.length === UUID.LENGTH,
30
+ new InvalidLengthError(this.value, UUID.LENGTH),
31
+ );
32
+ assert(UUID.PATTERN.test(value), new InvalidFormatError(this.value));
33
+ }
34
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './coordinates';
2
+ export * from './hashes';
2
3
  export * from './time';
3
4
  export * from './Color';
4
5
  export * from './Email';
@@ -7,3 +8,4 @@ export * from './Integer';
7
8
  export * from './NumberValueObject';
8
9
  export * from './PositiveNumber';
9
10
  export * from './StringValueObject';
11
+ export * from './NullObject';
@@ -1,7 +1,7 @@
1
1
  import { InvalidDayError } from '../../errors/InvalidDayError';
2
2
  import { InvalidDayFormatError } from '../../errors/InvalidDayFormatError';
3
- import { ValueObject } from '../../patterns';
4
3
  import { assert } from '../../patterns/Assert';
4
+ import { ValueObject } from '../ValueObject';
5
5
  import { Day } from './Day';
6
6
  import { DayOfWeek } from './DayOfWeek';
7
7
  import { Month } from './Month';
@@ -1,9 +1,5 @@
1
- import {
2
- Email,
3
- InvalidEmailError,
4
- NullObject,
5
- StringValueObject,
6
- } from '../../src';
1
+ import { Email, InvalidEmailError, StringValueObject } from '../../src';
2
+ import { NullObject } from '../../src/value-objects/NullObject';
7
3
 
8
4
  describe('Email', () => {
9
5
  describe('constructor', () => {