@schorts/shared-kernel 1.0.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.
Files changed (88) hide show
  1. package/.idx/airules.md +186 -0
  2. package/.idx/dev.nix +54 -0
  3. package/.vscode/settings.json +7 -0
  4. package/CHANGELOG +0 -0
  5. package/README.md +98 -0
  6. package/__tests__/auth/auth-provider.test.ts +45 -0
  7. package/__tests__/auth/require-auth.decorator.test.ts +88 -0
  8. package/__tests__/criteria/criteria.test.ts +159 -0
  9. package/__tests__/criteria/direction.test.ts +11 -0
  10. package/__tests__/criteria/filter-criterion.test.ts +15 -0
  11. package/__tests__/criteria/operator.test.ts +22 -0
  12. package/__tests__/criteria/order.test.ts +14 -0
  13. package/__tests__/domain-events/domain-event-primitives.test.ts +34 -0
  14. package/__tests__/domain-events/domain-event.test.ts +50 -0
  15. package/__tests__/entities/entity.test.ts +114 -0
  16. package/__tests__/formatters/pascal-camel-to-snake.test.ts +19 -0
  17. package/__tests__/http/fetch-http-provider.test.ts +155 -0
  18. package/__tests__/http/http-provider.test.ts +55 -0
  19. package/__tests__/json-api/json-api-connector.test.ts +78 -0
  20. package/__tests__/json-api/json-api-list.test.ts +24 -0
  21. package/__tests__/json-api/json-api-single.test.ts +24 -0
  22. package/__tests__/json-api/url-criteria-builder.test.ts +74 -0
  23. package/__tests__/messages/message.test.ts +16 -0
  24. package/__tests__/models/base-model.test.ts +10 -0
  25. package/__tests__/state-manager/state-manager.test.ts +101 -0
  26. package/__tests__/utils/url/url-with-params-builder.test.ts +39 -0
  27. package/__tests__/value-objects/coordinates-value.test.ts +68 -0
  28. package/__tests__/value-objects/email-value.test.ts +43 -0
  29. package/__tests__/value-objects/enum-value.test.ts +51 -0
  30. package/__tests__/value-objects/integer-value.test.ts +115 -0
  31. package/__tests__/value-objects/phone-value.test.ts +82 -0
  32. package/__tests__/value-objects/slug-value.test.ts +43 -0
  33. package/__tests__/value-objects/string-value.test.ts +121 -0
  34. package/__tests__/value-objects/uuid-value.test.ts +67 -0
  35. package/__tests__/value-objects/value-object.test.ts +25 -0
  36. package/jest.config.js +25 -0
  37. package/package.json +289 -0
  38. package/src/auth/auth-provider.ts +10 -0
  39. package/src/auth/exceptions/index.ts +1 -0
  40. package/src/auth/exceptions/not-authenticated.ts +1 -0
  41. package/src/auth/index.ts +3 -0
  42. package/src/auth/require-auth.decorator.ts +41 -0
  43. package/src/criteria/criteria.ts +51 -0
  44. package/src/criteria/direction.ts +1 -0
  45. package/src/criteria/exceptions/index.ts +2 -0
  46. package/src/criteria/exceptions/limit-not-valid.ts +1 -0
  47. package/src/criteria/exceptions/offset-not-valid.ts +1 -0
  48. package/src/criteria/filter-criterion.ts +6 -0
  49. package/src/criteria/index.ts +7 -0
  50. package/src/criteria/operator.ts +12 -0
  51. package/src/criteria/order.ts +4 -0
  52. package/src/domain-events/domain-event-primitives.ts +7 -0
  53. package/src/domain-events/domain-event.ts +15 -0
  54. package/src/domain-events/index.ts +2 -0
  55. package/src/entities/entity.ts +22 -0
  56. package/src/entities/index.ts +1 -0
  57. package/src/formatters/index.ts +1 -0
  58. package/src/formatters/pascal-camel-to-snake.ts +8 -0
  59. package/src/http/exceptions/http-exception.ts +8 -0
  60. package/src/http/exceptions/index.ts +1 -0
  61. package/src/http/fetch-http-provider.ts +120 -0
  62. package/src/http/http-provider.ts +7 -0
  63. package/src/http/index.ts +4 -0
  64. package/src/json-api/index.ts +4 -0
  65. package/src/json-api/json-api-connector.ts +56 -0
  66. package/src/json-api/json-api-list.ts +13 -0
  67. package/src/json-api/json-api-single.ts +13 -0
  68. package/src/json-api/url-criteria-builder.ts +49 -0
  69. package/src/messages/index.ts +1 -0
  70. package/src/messages/message.ts +3 -0
  71. package/src/models/base-model.ts +3 -0
  72. package/src/models/index.ts +1 -0
  73. package/src/state-manager/index.ts +1 -0
  74. package/src/state-manager/state-manager.ts +28 -0
  75. package/src/utils/index.ts +1 -0
  76. package/src/utils/url/index.ts +1 -0
  77. package/src/utils/url/url-with-params-builder.ts +19 -0
  78. package/src/value-objects/coordinates-value.ts +50 -0
  79. package/src/value-objects/email-value.ts +25 -0
  80. package/src/value-objects/enum-value.ts +25 -0
  81. package/src/value-objects/index.ts +10 -0
  82. package/src/value-objects/integer-value.ts +29 -0
  83. package/src/value-objects/phone-value.ts +53 -0
  84. package/src/value-objects/slug-value.ts +25 -0
  85. package/src/value-objects/string-value.ts +27 -0
  86. package/src/value-objects/uuid-value.ts +34 -0
  87. package/src/value-objects/value-object.ts +7 -0
  88. package/tsconfig.json +46 -0
@@ -0,0 +1,120 @@
1
+ import type { HTTPProvider } from "./http-provider";
2
+ import { HTTPException } from "./exceptions";
3
+
4
+ export class FetchHTTPProvider implements HTTPProvider {
5
+ private ongoingRequests = new Map<string, Promise<any>>();
6
+
7
+ get<ResponseType>(url: URL): Promise<ResponseType> {
8
+ return this.request("GET", url);
9
+ }
10
+
11
+ post<RequestBodySchema, ResponseType>(
12
+ url: URL,
13
+ body: RequestBodySchema
14
+ ): Promise<ResponseType> {
15
+ return this.request("POST", url, body);
16
+ }
17
+
18
+ put<RequestBodySchema, ResponseType>(
19
+ url: URL,
20
+ body: RequestBodySchema
21
+ ): Promise<ResponseType> {
22
+ return this.request("PUT", url, body);
23
+ }
24
+
25
+ patch<RequestBodySchema, ResponseType>(
26
+ url: URL,
27
+ body: RequestBodySchema
28
+ ): Promise<ResponseType> {
29
+ return this.request("PATCH", url, body);
30
+ }
31
+
32
+ delete<ResponseType>(url: URL): Promise<ResponseType> {
33
+ return this.request("DELETE", url);
34
+ }
35
+
36
+ private async request<ResponseType>(
37
+ method: string,
38
+ url: URL,
39
+ body?: unknown
40
+ ): Promise<ResponseType> {
41
+ const key = this.generateRequestKey(method, url, body);
42
+
43
+ if (this.ongoingRequests.has(key)) {
44
+ return this.ongoingRequests.get(key) as Promise<ResponseType>;
45
+ }
46
+
47
+ const init: RequestInit = {
48
+ method,
49
+ body: body !== undefined ? JSON.stringify(body) : null,
50
+ };
51
+
52
+ if (body !== undefined) {
53
+ init.headers = { "Content-Type": "application/json" };
54
+ }
55
+
56
+ const request = (async () => {
57
+ const response = await fetch(url.href, init);
58
+
59
+ if (!response) {
60
+ throw new HTTPException("Fetch returned undefined", 0);
61
+ }
62
+
63
+ if (response.status === 204) {
64
+ return undefined as ResponseType;
65
+ }
66
+
67
+ const contentType = response.headers.get("Content-Type") ?? "";
68
+ let parsed: any;
69
+
70
+ try {
71
+ if (contentType.includes("application/json")) {
72
+ parsed = await response.json();
73
+ } else if (contentType.includes("text/")) {
74
+ parsed = await response.text();
75
+ } else {
76
+ parsed = await response.blob();
77
+ }
78
+ } catch {
79
+ parsed = undefined;
80
+ }
81
+
82
+ if (!response.ok) {
83
+ const message =
84
+ typeof parsed === "string"
85
+ ? parsed
86
+ : parsed?.title ?? "Unknown error";
87
+ const code = parsed?.code ?? response.status;
88
+
89
+ throw new HTTPException(message, code);
90
+ }
91
+
92
+ return parsed as ResponseType;
93
+ })().finally(() => {
94
+ this.ongoingRequests.delete(key);
95
+ });
96
+
97
+ this.ongoingRequests.set(key, request);
98
+
99
+ return request;
100
+ }
101
+
102
+ private generateRequestKey(method: string, url: URL, body?: unknown): string {
103
+ const base = `${method}:${url.href}`;
104
+ const bodyHash = body !== undefined ? this.hashString(JSON.stringify(body)) : "";
105
+
106
+ return `${base}:${bodyHash}`;
107
+ }
108
+
109
+ private hashString(input: string): string {
110
+ let hash = 0;
111
+
112
+ for (let i = 0; i < input.length; i++) {
113
+ const chr = input.charCodeAt(i);
114
+ hash = (hash << 5) - hash + chr;
115
+ hash |= 0;
116
+ }
117
+
118
+ return hash.toString();
119
+ }
120
+ }
@@ -0,0 +1,7 @@
1
+ export interface HTTPProvider {
2
+ get<ResponseType>(url: URL): Promise<ResponseType>;
3
+ post<RequestBodySchema, ResponseType>(url: URL, body: RequestBodySchema): Promise<ResponseType>;
4
+ put<RequestBodySchema, ResponseType>(url: URL, body: RequestBodySchema): Promise<ResponseType>;
5
+ patch<RequestBodySchema, ResponseType>(url: URL, body: RequestBodySchema): Promise<ResponseType>;
6
+ delete<ResponseType>(url: URL): Promise<ResponseType>;
7
+ }
@@ -0,0 +1,4 @@
1
+ export type { HTTPProvider } from "./http-provider";
2
+
3
+ export { HTTPException } from "./exceptions";
4
+ export { FetchHTTPProvider } from "./fetch-http-provider";
@@ -0,0 +1,4 @@
1
+ export type { JSONAPISingle } from "./json-api-single";
2
+ export type { JSONAPIList } from "./json-api-list";
3
+ export { URLCriteriaBuilder } from "./url-criteria-builder";
4
+ export { JSONAPIConnector } from "./json-api-connector";
@@ -0,0 +1,56 @@
1
+ import type { HTTPProvider } from "../http";
2
+ import type { JSONAPIList } from "./json-api-list";
3
+ import type { JSONAPISingle } from "./json-api-single";
4
+ import { URLCriteriaBuilder } from "./url-criteria-builder";
5
+ import { Criteria } from "../criteria";
6
+
7
+ export class JSONAPIConnector {
8
+ constructor(private http: HTTPProvider) {}
9
+
10
+ async findOne<EntityAttributes>(
11
+ url: URL,
12
+ criteria?: Criteria,
13
+ include?: string[]
14
+ ): Promise<JSONAPISingle<EntityAttributes>> {
15
+ const fullUrl = new URLCriteriaBuilder(url, criteria, include).build();
16
+
17
+ return this.http.get(fullUrl);
18
+ }
19
+
20
+ async findMany<EntityAttributes>(
21
+ url: URL,
22
+ criteria?: Criteria,
23
+ include?: string[]
24
+ ): Promise<JSONAPIList<EntityAttributes>> {
25
+ const fullUrl = new URLCriteriaBuilder(url, criteria, include).build();
26
+
27
+ return this.http.get(fullUrl);
28
+ }
29
+
30
+ async create<EntityAttributes>(
31
+ url: URL,
32
+ payload: {
33
+ type: string;
34
+ attributes: Omit<EntityAttributes, "id">;
35
+ }
36
+ ): Promise<JSONAPISingle<EntityAttributes>> {
37
+ return this.http.post(url, { data: payload });
38
+ }
39
+
40
+ async update<EntityAttributes>(
41
+ url: URL,
42
+ payload: {
43
+ id: string;
44
+ type: string;
45
+ attributes: Omit<EntityAttributes, "id">;
46
+ }
47
+ ): Promise<JSONAPISingle<EntityAttributes>> {
48
+ return this.http.patch(url, { data: payload });
49
+ }
50
+
51
+ async delete<EntityAttributes>(
52
+ url: URL
53
+ ): Promise<JSONAPISingle<EntityAttributes>> {
54
+ return this.http.delete(url);
55
+ }
56
+ }
@@ -0,0 +1,13 @@
1
+ export type JSONAPIList<EntityAttributes> = {
2
+ data: Array<{
3
+ id: string;
4
+ type: string;
5
+ attributes: Omit<EntityAttributes, "id">;
6
+ }>;
7
+ included?: Array<{
8
+ id: string;
9
+ type: string;
10
+ attributes: Record<string, any>;
11
+ }>;
12
+ meta?: Record<string, any>;
13
+ };
@@ -0,0 +1,13 @@
1
+ export type JSONAPISingle<EntityAttributes> = {
2
+ data: {
3
+ id: string;
4
+ type: string;
5
+ attributes: Omit<EntityAttributes, "id">;
6
+ };
7
+ included?: Array<{
8
+ id: string;
9
+ type: string;
10
+ attributes: Record<string, any>;
11
+ }>;
12
+ meta?: Record<string, any>;
13
+ };
@@ -0,0 +1,49 @@
1
+ import { URLWithParamsBuilder } from "../utils";
2
+ import { Criteria } from "../criteria";
3
+
4
+ export class URLCriteriaBuilder {
5
+ constructor(
6
+ private readonly base: URL,
7
+ private readonly criteria?: Criteria,
8
+ private readonly include?: string[]
9
+ ) {}
10
+
11
+ build(): URL {
12
+ const builder = new URLWithParamsBuilder(new URL(this.base.href));
13
+
14
+ if (this.include?.length) {
15
+ builder.with({ include: this.include });
16
+ }
17
+
18
+ if (this.criteria) {
19
+ Object.entries(this.criteria.filters).forEach(([field, { operator, value }]) => {
20
+ const encodedField = field.replace(/\./g, "."); // allows nested filters like roles.name
21
+
22
+ if (operator === "EQUAL") {
23
+ builder.with({ [`filter[${encodedField}]`]: value });
24
+ } else if (operator === "IN" && Array.isArray(value)) {
25
+ builder.with({ [`filter[${encodedField}]`]: value.join(",") });
26
+ } else {
27
+ builder.with({ [`filter[${encodedField}][${operator}]`]: value });
28
+ }
29
+ });
30
+
31
+ if (this.criteria.orders.length > 0) {
32
+ const sortParam = this.criteria.orders
33
+ .map(({ field, direction }) => (direction === "DESC" ? `-${field}` : field))
34
+ .join(",");
35
+ builder.with({ sort: sortParam });
36
+ }
37
+
38
+ if (this.criteria.limit !== undefined) {
39
+ builder.with({ "page[limit]": this.criteria.limit });
40
+ }
41
+
42
+ if (this.criteria.offset !== undefined) {
43
+ builder.with({ "page[offset]": this.criteria.offset });
44
+ }
45
+ }
46
+
47
+ return builder.build();
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export type { Message } from "./message";
@@ -0,0 +1,3 @@
1
+ export interface Message<Primitives = {}> {
2
+ toPrimitives(): Primitives;
3
+ }
@@ -0,0 +1,3 @@
1
+ export type BaseModel = {
2
+ id: string | number;
3
+ };
@@ -0,0 +1 @@
1
+ export type { BaseModel } from "./base-model";
@@ -0,0 +1 @@
1
+ export { StateManager } from "./state-manager";
@@ -0,0 +1,28 @@
1
+ export abstract class StateManager<Schema extends Record<string, any>> {
2
+ protected state: Schema;
3
+ private listeners: Array<(state: Schema) => void> = [];
4
+
5
+ constructor(initialState: Schema = {} as Schema) {
6
+ this.state = initialState;
7
+ }
8
+
9
+ abstract getValue<Key extends keyof Schema>(key: Key): Promise<Schema[Key]>;
10
+ abstract setValue<Key extends keyof Schema>(key: Key, value: Schema[Key]): Promise<void>;
11
+ abstract removeValue<Key extends keyof Schema>(key: Key): Promise<void>;
12
+
13
+ getState(): Schema {
14
+ return this.state;
15
+ }
16
+
17
+ subscribe(listener: (state: Schema) => void): () => void {
18
+ this.listeners.push(listener);
19
+
20
+ return () => {
21
+ this.listeners = this.listeners.filter(l => l !== listener);
22
+ };
23
+ }
24
+
25
+ protected notifyListeners(): void {
26
+ this.listeners.forEach(listener => listener(this.state));
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export { URLWithParamsBuilder } from "./url";
@@ -0,0 +1 @@
1
+ export { URLWithParamsBuilder } from "./url-with-params-builder";
@@ -0,0 +1,19 @@
1
+ export class URLWithParamsBuilder {
2
+ constructor(private readonly base: URL) {}
3
+
4
+ with(params: Record<string, string | number | boolean | Array<string | number>>): URLWithParamsBuilder {
5
+ Object.entries(params).forEach(([key, value]) => {
6
+ if (Array.isArray(value)) {
7
+ value.forEach(v => this.base.searchParams.append(key, String(v)));
8
+ } else {
9
+ this.base.searchParams.set(key, String(value));
10
+ }
11
+ });
12
+
13
+ return this;
14
+ }
15
+
16
+ build(): URL {
17
+ return this.base;
18
+ }
19
+ }
@@ -0,0 +1,50 @@
1
+ import { ValueObject } from "./";
2
+
3
+ const EPSILON = 1e-6;
4
+
5
+ export abstract class CoordinatesValue implements ValueObject {
6
+ readonly valueType = "Coordinates";
7
+ readonly value: {
8
+ latitude: number,
9
+ longitude: number,
10
+ };
11
+
12
+ constructor(value: CoordinatesValue["value"]) {
13
+ this.value = value;
14
+ }
15
+
16
+ get isValid(): boolean {
17
+ const validLatitud = -90 <= this.value.latitude && this.value.latitude <= 90;
18
+ const validLongitude = -180 <= this.value.longitude && this.value.longitude <= 180;
19
+
20
+ return validLatitud && validLongitude;
21
+ }
22
+
23
+ get latitude(): number | null {
24
+ if (this.isValid) {
25
+ return this.value.latitude;
26
+ }
27
+
28
+ return null
29
+ }
30
+
31
+ get longitude(): number | null {
32
+ if (this.isValid) {
33
+ return this.value.longitude;
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ equals(valueObject: unknown): boolean {
40
+ if (!(valueObject instanceof CoordinatesValue)) return false;
41
+ if (!this.isValid || !valueObject.isValid) return false;
42
+
43
+ const latDiff = Math.abs(this.value.latitude - valueObject.value.latitude);
44
+ const lonDiff = Math.abs(this.value.longitude - valueObject.value.longitude);
45
+
46
+ return latDiff < EPSILON && lonDiff < EPSILON;
47
+ }
48
+
49
+ abstract readonly attributeName: string;
50
+ }
@@ -0,0 +1,25 @@
1
+ import { ValueObject } from "./";
2
+
3
+ const REGEX = /^([A-Za-z0-9_\-\.])+\@(?!(?:[A-Za-z0-9_\-\.]+\.)?com\.com)([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
4
+
5
+ export abstract class EmailValue implements ValueObject {
6
+ readonly valueType = "Email";
7
+ readonly value: string;
8
+
9
+ constructor(value: string) {
10
+ this.value = value;
11
+ }
12
+
13
+ get isValid(): boolean {
14
+ return REGEX.test(this.value);
15
+ }
16
+
17
+ equals(valueObject: unknown): boolean {
18
+ if (!(valueObject instanceof EmailValue)) return false;
19
+ if (!this.isValid || !valueObject.isValid) return false;
20
+
21
+ return this.value === valueObject.value;
22
+ }
23
+
24
+ abstract readonly attributeName: string;
25
+ }
@@ -0,0 +1,25 @@
1
+ import { ValueObject } from "./";
2
+
3
+ export abstract class EnumValue implements ValueObject {
4
+ readonly valueType = "Enum";
5
+ readonly allowedValues: Array<string>;
6
+ readonly value: typeof this.allowedValues[number] | string;
7
+
8
+ constructor(allowedValues: Array<string>, value: string) {
9
+ this.allowedValues = allowedValues;
10
+ this.value = value;
11
+ }
12
+
13
+ get isValid(): boolean {
14
+ return this.allowedValues.includes(this.value);
15
+ }
16
+
17
+ equals(valueObject: unknown): boolean {
18
+ if (!(valueObject instanceof EnumValue)) return false;
19
+ if (!this.isValid || !valueObject.isValid) return false;
20
+
21
+ return this.value === valueObject.value;
22
+ }
23
+
24
+ abstract readonly attributeName: string;
25
+ }
@@ -0,0 +1,10 @@
1
+ export type { ValueObject } from "./value-object";
2
+
3
+ export { UUIDValue } from "./uuid-value";
4
+ export { StringValue } from "./string-value";
5
+ export { EnumValue } from "./enum-value";
6
+ export { EmailValue } from "./email-value";
7
+ export { PhoneValue } from "./phone-value";
8
+ export { SlugValue } from "./slug-value";
9
+ export { IntegerValue } from "./integer-value";
10
+ export { CoordinatesValue } from "./coordinates-value";
@@ -0,0 +1,29 @@
1
+ import { ValueObject } from "./";
2
+
3
+ export abstract class IntegerValue implements ValueObject {
4
+ readonly valueType = "Integer";
5
+ readonly min: number | undefined;
6
+ readonly max: number | undefined;
7
+ readonly value: number;
8
+
9
+ constructor(value: number, min?: number, max?: number) {
10
+ this.min = min;
11
+ this.max = max;
12
+ this.value = value;
13
+ }
14
+
15
+ get isValid(): boolean {
16
+ return (this.min ? this.value >= this.min : true)
17
+ && (this.max ? this.value <= this.max : true)
18
+ && Number.isInteger(this.value);
19
+ }
20
+
21
+ equals(valueObject: unknown): boolean {
22
+ if (!(valueObject instanceof IntegerValue)) return false;
23
+ if (!this.isValid || !valueObject.isValid) return false;
24
+
25
+ return this.value === valueObject.value;
26
+ }
27
+
28
+ abstract readonly attributeName: string;
29
+ }
@@ -0,0 +1,53 @@
1
+ import { ValueObject } from "./";
2
+
3
+ const REGEX = /\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{10,12}$/;
4
+
5
+ export abstract class PhoneValue implements ValueObject {
6
+ readonly valueType = "Phone";
7
+ readonly value: string;
8
+
9
+ constructor(value: string) {
10
+ this.value = value;
11
+ }
12
+
13
+ get isValid(): boolean {
14
+ return REGEX.test(this.value);
15
+ }
16
+
17
+ get countryCode(): string | null {
18
+ if (this.isValid) {
19
+ const countryCodeLength = this.value.length - 10;
20
+
21
+ return this.value.slice(0, countryCodeLength);
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ get phoneNumber(): string | null {
28
+ if (this.isValid) {
29
+ return this.value.slice(-10);
30
+ }
31
+
32
+ return null;
33
+ }
34
+
35
+ get formattedPhone(): string | null {
36
+ if (this.isValid) {
37
+ const phoneNumber = this.phoneNumber!.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");
38
+
39
+ return `${this.countryCode} ${phoneNumber}`;
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ equals(valueObject: unknown): boolean {
46
+ if (!(valueObject instanceof PhoneValue)) return false;
47
+ if (!this.isValid || !valueObject.isValid) return false;
48
+
49
+ return this.value === valueObject.value;
50
+ }
51
+
52
+ abstract readonly attributeName: string;
53
+ }
@@ -0,0 +1,25 @@
1
+ import { ValueObject } from "./";
2
+
3
+ const REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4
+
5
+ export abstract class SlugValue implements ValueObject {
6
+ readonly valueType = "Slug";
7
+ readonly value: string;
8
+
9
+ constructor(value: string) {
10
+ this.value = value;
11
+ }
12
+
13
+ get isValid(): boolean {
14
+ return REGEX.test(this.value);
15
+ }
16
+
17
+ equals(valueObject: unknown): boolean {
18
+ if (!(valueObject instanceof SlugValue)) return false;
19
+ if (!this.isValid || !valueObject.isValid) return false;
20
+
21
+ return this.value === valueObject.value;
22
+ }
23
+
24
+ abstract readonly attributeName: string;
25
+ }
@@ -0,0 +1,27 @@
1
+ import { ValueObject } from "./";
2
+
3
+ export abstract class StringValue implements ValueObject {
4
+ readonly valueType = "String";
5
+ readonly value: string;
6
+ readonly minLength: number;
7
+ readonly maxLength: number | undefined;
8
+
9
+ constructor(value: string, minLength = 0, maxLength?: number) {
10
+ this.value = value;
11
+ this.minLength = minLength;
12
+ this.maxLength = maxLength;
13
+ }
14
+
15
+ get isValid(): boolean {
16
+ return this.value.length >= this.minLength && (this.maxLength ? this.value.length <= this.maxLength : true);
17
+ }
18
+
19
+ equals(valueObject: unknown): boolean {
20
+ if (!(valueObject instanceof StringValue)) return false;
21
+ if (!this.isValid || !valueObject.isValid) return false;
22
+
23
+ return this.value === valueObject.value;
24
+ }
25
+
26
+ abstract readonly attributeName: string;
27
+ }
@@ -0,0 +1,34 @@
1
+ import type { ValueObject } from "./";
2
+
3
+ const REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+
5
+ type UUIDInput<Required extends boolean> =
6
+ Required extends true ? string : string | undefined;
7
+
8
+ export abstract class UUIDValue<Required extends boolean = true> implements ValueObject {
9
+ readonly valueType = "UUID";
10
+ readonly value: UUIDInput<Required>;
11
+ private readonly optional: boolean;
12
+
13
+ constructor(value: UUIDInput<Required>, optional: boolean = false) {
14
+ this.value = value;
15
+ this.optional = optional;
16
+ }
17
+
18
+ get isValid(): boolean {
19
+ return this.optional
20
+ ? this.value
21
+ ? REGEX.test(this.value)
22
+ : true
23
+ : REGEX.test(this.value!);
24
+ }
25
+
26
+ equals(valueObject: unknown): boolean {
27
+ if (!(valueObject instanceof UUIDValue)) return false;
28
+ if (!this.isValid || !valueObject.isValid) return false;
29
+
30
+ return this.value === valueObject.value;
31
+ }
32
+
33
+ abstract readonly attributeName: string;
34
+ }
@@ -0,0 +1,7 @@
1
+ export interface ValueObject {
2
+ readonly value: unknown;
3
+ readonly valueType: string;
4
+ readonly attributeName: string;
5
+ isValid: boolean;
6
+ equals(valueObject: unknown): boolean;
7
+ }