-
Notifications
You must be signed in to change notification settings - Fork 0
/
rest-repository.ts
129 lines (115 loc) · 3.73 KB
/
rest-repository.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import type { Constructor } from "./constructor.ts";
import type { Repository } from "./repository.ts";
import { Result } from "./result.ts";
/**
* Encapsulates the logic required to access data sources
* exposed as RESTful APIs.
*/
export class RESTRepository<T extends object> implements Repository<T> {
static init: Partial<RequestInit> = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
constructor(
public Entity: Constructor<T>,
public url: string,
public idProperty: string = "_id",
) {}
has(id: string): Promise<Result<boolean, undefined>> {
return Promise.resolve(Result.ok(!!id));
}
async query<U>(
search?: Record<string, string>,
path = "",
): Promise<Result<U | undefined, Response | TypeError>> {
let { url } = this;
if (path) url += path;
if (search) url += `?${new URLSearchParams(search).toString()}`;
return await (this.constructor as typeof RESTRepository).fetch<U>(url);
}
async command<U>(
body: unknown,
path = "",
): Promise<Result<U | undefined, Response | TypeError>> {
let { url } = this;
if (path) url += path;
return await (this.constructor as typeof RESTRepository).fetch<U>(url, {
method: "POST",
body: JSON.stringify(body),
});
}
async list(
search?: Record<string, string>,
): Promise<Result<Array<T>, undefined | Response | TypeError>> {
const result = await this.query<Array<string>>(search);
if (!result.ok) return result;
if (result.value === undefined) return Result.fail(undefined);
return Result.ok(result.value.map((i) => this.deserialize(i)));
}
create(value: T): Promise<Result<unknown, Response | TypeError>> {
return (this.constructor as typeof RESTRepository).fetch(
`${this.url}`,
{ method: "POST", body: this.serialize(value) as string },
);
}
async read(id: string): Promise<Result<T, Response | TypeError>> {
const result = await (this.constructor as typeof RESTRepository).fetch(
`${this.url}/${id}`,
);
if (!result.ok) return result;
return Result.ok(this.deserialize(result.value));
}
update(
id: string,
updates: Partial<T>,
): Promise<Result<unknown, Response | TypeError>> {
return (this.constructor as typeof RESTRepository).fetch<unknown>(
`${this.url}/${id}`,
{ method: "PUT", body: this.serialize(updates) as string },
);
}
delete(id: string): Promise<Result<undefined, Response | TypeError>> {
return (this.constructor as typeof RESTRepository).fetch<undefined>(
`${this.url}/${id}`,
{ method: "DELETE" },
);
}
serialize(entity: Partial<T>): unknown {
return JSON.stringify(entity);
}
deserialize(value: unknown): T {
return new this.Entity(value);
}
async save(value: T): Promise<Result<unknown, Response | TypeError>> {
const id = value[this.idProperty as keyof T] as unknown as string;
const exists = await this.has(id);
if (exists.value) {
return this.update(id, value);
}
return this.create(value);
}
static async fetch<T>(
url: string,
init: Partial<RequestInit> = {},
): Promise<Result<T | undefined, Response | TypeError>> {
try {
const response = await globalThis.fetch(url, { ...this.init, ...init });
if (response.ok || response.status === 304) {
const contentType = response.headers.get("Content-Type");
if (contentType) {
const body: T = contentType.includes("application/json")
? await response.json()
: await response.arrayBuffer();
return Result.ok(body);
}
return Result.ok(undefined);
} else {
return Result.fail(response);
}
} catch (e) {
return Result.fail(e);
}
}
}