@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.
- package/.idx/airules.md +186 -0
- package/.idx/dev.nix +54 -0
- package/.vscode/settings.json +7 -0
- package/CHANGELOG +0 -0
- package/README.md +98 -0
- package/__tests__/auth/auth-provider.test.ts +45 -0
- package/__tests__/auth/require-auth.decorator.test.ts +88 -0
- package/__tests__/criteria/criteria.test.ts +159 -0
- package/__tests__/criteria/direction.test.ts +11 -0
- package/__tests__/criteria/filter-criterion.test.ts +15 -0
- package/__tests__/criteria/operator.test.ts +22 -0
- package/__tests__/criteria/order.test.ts +14 -0
- package/__tests__/domain-events/domain-event-primitives.test.ts +34 -0
- package/__tests__/domain-events/domain-event.test.ts +50 -0
- package/__tests__/entities/entity.test.ts +114 -0
- package/__tests__/formatters/pascal-camel-to-snake.test.ts +19 -0
- package/__tests__/http/fetch-http-provider.test.ts +155 -0
- package/__tests__/http/http-provider.test.ts +55 -0
- package/__tests__/json-api/json-api-connector.test.ts +78 -0
- package/__tests__/json-api/json-api-list.test.ts +24 -0
- package/__tests__/json-api/json-api-single.test.ts +24 -0
- package/__tests__/json-api/url-criteria-builder.test.ts +74 -0
- package/__tests__/messages/message.test.ts +16 -0
- package/__tests__/models/base-model.test.ts +10 -0
- package/__tests__/state-manager/state-manager.test.ts +101 -0
- package/__tests__/utils/url/url-with-params-builder.test.ts +39 -0
- package/__tests__/value-objects/coordinates-value.test.ts +68 -0
- package/__tests__/value-objects/email-value.test.ts +43 -0
- package/__tests__/value-objects/enum-value.test.ts +51 -0
- package/__tests__/value-objects/integer-value.test.ts +115 -0
- package/__tests__/value-objects/phone-value.test.ts +82 -0
- package/__tests__/value-objects/slug-value.test.ts +43 -0
- package/__tests__/value-objects/string-value.test.ts +121 -0
- package/__tests__/value-objects/uuid-value.test.ts +67 -0
- package/__tests__/value-objects/value-object.test.ts +25 -0
- package/jest.config.js +25 -0
- package/package.json +289 -0
- package/src/auth/auth-provider.ts +10 -0
- package/src/auth/exceptions/index.ts +1 -0
- package/src/auth/exceptions/not-authenticated.ts +1 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/require-auth.decorator.ts +41 -0
- package/src/criteria/criteria.ts +51 -0
- package/src/criteria/direction.ts +1 -0
- package/src/criteria/exceptions/index.ts +2 -0
- package/src/criteria/exceptions/limit-not-valid.ts +1 -0
- package/src/criteria/exceptions/offset-not-valid.ts +1 -0
- package/src/criteria/filter-criterion.ts +6 -0
- package/src/criteria/index.ts +7 -0
- package/src/criteria/operator.ts +12 -0
- package/src/criteria/order.ts +4 -0
- package/src/domain-events/domain-event-primitives.ts +7 -0
- package/src/domain-events/domain-event.ts +15 -0
- package/src/domain-events/index.ts +2 -0
- package/src/entities/entity.ts +22 -0
- package/src/entities/index.ts +1 -0
- package/src/formatters/index.ts +1 -0
- package/src/formatters/pascal-camel-to-snake.ts +8 -0
- package/src/http/exceptions/http-exception.ts +8 -0
- package/src/http/exceptions/index.ts +1 -0
- package/src/http/fetch-http-provider.ts +120 -0
- package/src/http/http-provider.ts +7 -0
- package/src/http/index.ts +4 -0
- package/src/json-api/index.ts +4 -0
- package/src/json-api/json-api-connector.ts +56 -0
- package/src/json-api/json-api-list.ts +13 -0
- package/src/json-api/json-api-single.ts +13 -0
- package/src/json-api/url-criteria-builder.ts +49 -0
- package/src/messages/index.ts +1 -0
- package/src/messages/message.ts +3 -0
- package/src/models/base-model.ts +3 -0
- package/src/models/index.ts +1 -0
- package/src/state-manager/index.ts +1 -0
- package/src/state-manager/state-manager.ts +28 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/url/index.ts +1 -0
- package/src/utils/url/url-with-params-builder.ts +19 -0
- package/src/value-objects/coordinates-value.ts +50 -0
- package/src/value-objects/email-value.ts +25 -0
- package/src/value-objects/enum-value.ts +25 -0
- package/src/value-objects/index.ts +10 -0
- package/src/value-objects/integer-value.ts +29 -0
- package/src/value-objects/phone-value.ts +53 -0
- package/src/value-objects/slug-value.ts +25 -0
- package/src/value-objects/string-value.ts +27 -0
- package/src/value-objects/uuid-value.ts +34 -0
- package/src/value-objects/value-object.ts +7 -0
- 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,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 @@
|
|
|
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
|
+
}
|