UNPKG

10.1 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Resource = void 0;
4const state_1 = require("./state");
5const uri_1 = require("./util/uri");
6const follow_promise_1 = require("./follow-promise");
7const link_1 = require("./link");
8const events_1 = require("events");
9const fetch_body_helper_1 = require("./util/fetch-body-helper");
10/**
11 * A 'resource' represents an endpoint on a server.
12 *
13 * A resource has a uri, methods that correspond to HTTP methods,
14 * and events to subscribe to state changes.
15 */
16class Resource extends events_1.EventEmitter {
17 /**
18 * Create the resource.
19 *
20 * This is usually done by the Client.
21 */
22 constructor(client, uri) {
23 super();
24 this.client = client;
25 this.uri = uri;
26 this.activeRefresh = null;
27 }
28 /**
29 * Gets the current state of the resource.
30 *
31 * This function will return a State object.
32 */
33 get(getOptions) {
34 const state = this.client.cache.get(this.uri);
35 if (!state) {
36 return this.refresh(getOptions);
37 }
38 return Promise.resolve(state);
39 }
40 /**
41 * Does a HEAD request and returns a HeadState object.
42 *
43 * If there was a valid existing cache for a GET request, it will
44 * still return that.
45 */
46 async head(headOptions) {
47 let state = this.client.cache.get(this.uri);
48 if (state) {
49 return state;
50 }
51 const response = await this.fetchOrThrow(optionsToRequestInit('HEAD', headOptions));
52 state = await state_1.headStateFactory(this.uri, response);
53 return state;
54 }
55 /**
56 * Gets the current state of the resource, skipping
57 * the cache.
58 *
59 * This function will return a State object.
60 */
61 refresh(getOptions) {
62 const params = {
63 cache: 'reload',
64 };
65 if ((getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && !(getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
66 params.headers = getOptions.getContentHeaders();
67 }
68 else if (!(getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && (getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
69 params.headers = getOptions.headers;
70 }
71 else if ((getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && (getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
72 params.headers = getOptions.getContentHeaders();
73 params.headers = Object.assign(Object.assign({}, getOptions.headers), params.headers);
74 }
75 if (!this.activeRefresh) {
76 this.activeRefresh = (async () => {
77 try {
78 const response = await this.fetchOrThrow(params);
79 const state = await this.client.getStateForResponse(this.uri, response);
80 this.updateCache(state);
81 return state;
82 }
83 finally {
84 this.activeRefresh = null;
85 }
86 })();
87 }
88 return this.activeRefresh;
89 }
90 /**
91 * Updates the server state with a PUT request
92 */
93 async put(options) {
94 const requestInit = optionsToRequestInit('PUT', options);
95 /**
96 * If we got a 'State' object passed, it means we don't need to emit a
97 * stale event, as the passed object is the new
98 * state.
99 *
100 * We're gonna track that with a custom header that will be removed
101 * later in the fetch pipeline.
102 */
103 if (state_1.isState(options)) {
104 requestInit.headers.set('X-KETTING-NO-STALE', '1');
105 }
106 await this.fetchOrThrow(requestInit);
107 if (state_1.isState(options)) {
108 this.updateCache(options);
109 }
110 }
111 /**
112 * Deletes the resource
113 */
114 async delete() {
115 await this.fetchOrThrow({ method: 'DELETE' });
116 }
117 /**
118 * Sends a POST request to the resource.
119 *
120 * See the documentation for PostRequestOptions for more details.
121 * This function is used for RPC-like endpoints and form submissions.
122 *
123 * This function will return the response as a State object.
124 */
125 async post(options) {
126 const response = await this.fetchOrThrow(optionsToRequestInit('POST', options));
127 return this.client.getStateForResponse(this.uri, response);
128 }
129 /**
130 * Sends a POST request, and follows to the next resource.
131 *
132 * If a server responds with a 201 Status code and a Location header,
133 * it will automatically return the newly created resource.
134 *
135 * If the server responded with a 204 or 205, this function will return
136 * `this`.
137 */
138 async postFollow(options) {
139 const response = await this.fetchOrThrow(optionsToRequestInit('POST', options));
140 switch (response.status) {
141 case 201:
142 if (response.headers.has('location')) {
143 return this.go(response.headers.get('location'));
144 }
145 throw new Error('Could not follow after a 201 request, because the server did not reply with a Location header');
146 case 204:
147 case 205:
148 return this;
149 default:
150 throw new Error('Did not receive a 201, 204 or 205 status code so we could not follow to the next resource');
151 }
152 }
153 /**
154 * Sends a PATCH request to the resource.
155 *
156 * This function defaults to a application/json content-type header.
157 *
158 * If the server responds with 200 Status code this will return a State object
159 */
160 async patch(options) {
161 const response = await this.fetchOrThrow(optionsToRequestInit('PATCH', options));
162 if (response.status === 200) {
163 return await this.client.getStateForResponse(this.uri, response);
164 }
165 }
166 /**
167 * Follows a relationship, based on its reltype. For example, this might be
168 * 'alternate', 'item', 'edit' or a custom url-based one.
169 *
170 * This function can also follow templated uris. You can specify uri
171 * variables in the optional variables argument.
172 */
173 follow(rel, variables) {
174 return new follow_promise_1.FollowPromiseOne(this, rel, variables);
175 }
176 /**
177 * Follows a relationship based on its reltype. This function returns a
178 * Promise that resolves to an array of Resource objects.
179 *
180 * If no resources were found, the array will be empty.
181 */
182 followAll(rel) {
183 return new follow_promise_1.FollowPromiseMany(this, rel);
184 }
185 /**
186 * Resolves a new resource based on a relative uri.
187 *
188 * Use this function to manually get a Resource object via a uri. The uri
189 * will be resolved based on the uri of the current resource.
190 *
191 * This function doesn't do any HTTP requests.
192 */
193 go(uri) {
194 uri = uri_1.resolve(this.uri, uri);
195 return this.client.go(uri);
196 }
197 /**
198 * Does a HTTP request on the current resource URI
199 */
200 fetch(init) {
201 return this.client.fetcher.fetch(this.uri, init);
202 }
203 /**
204 * Does a HTTP request on the current resource URI.
205 *
206 * If the response was a 4XX or 5XX, this function will throw
207 * an exception.
208 */
209 fetchOrThrow(init) {
210 return this.client.fetcher.fetchOrThrow(this.uri, init);
211 }
212 /**
213 * Updates the state cache, and emits events.
214 *
215 * This will update the local state but *not* update the server
216 */
217 updateCache(state) {
218 if (state.uri !== this.uri) {
219 throw new Error('When calling updateCache on a resource, the uri of the State object must match the uri of the Resource');
220 }
221 this.client.cacheState(state);
222 }
223 /**
224 * Clears the state cache for this resource.
225 */
226 clearCache() {
227 this.client.cache.delete(this.uri);
228 }
229 /**
230 * Returns a Link object, by its REL.
231 *
232 * If the link does not exist, a LinkNotFound error will be thrown.
233 *
234 * @deprecated
235 */
236 async link(rel) {
237 const state = await this.get();
238 const link = state.links.get(rel);
239 if (!link) {
240 throw new link_1.LinkNotFound(`Link with rel: ${rel} not found on ${this.uri}`);
241 }
242 return link;
243 }
244 /**
245 * Returns all links defined on this object.
246 *
247 * @deprecated
248 */
249 async links(rel) {
250 const state = await this.get();
251 if (!rel) {
252 return state.links.getAll();
253 }
254 else {
255 return state.links.getMany(rel);
256 }
257 }
258 /**
259 *
260 * Returns true or false depending on if a link with the specified relation
261 * type exists.
262 *
263 * @deprecated
264 */
265 async hasLink(rel) {
266 const state = await this.get();
267 return state.links.has(rel);
268 }
269}
270exports.Resource = Resource;
271exports.default = Resource;
272function optionsToRequestInit(method, options) {
273 if (!options) {
274 return {
275 method,
276 headers: new Headers(),
277 };
278 }
279 let headers;
280 if (options.getContentHeaders) {
281 headers = new Headers(options.getContentHeaders());
282 }
283 else if (options.headers) {
284 headers = new Headers(options.headers);
285 }
286 else {
287 headers = new Headers();
288 }
289 if (!headers.has('Content-Type')) {
290 headers.set('Content-Type', 'application/json');
291 }
292 let body;
293 if (options.serializeBody !== undefined) {
294 body = options.serializeBody();
295 }
296 else if (options.data) {
297 body = options.data;
298 if (fetch_body_helper_1.needsJsonStringify(body)) {
299 body = JSON.stringify(body);
300 }
301 }
302 else {
303 body = null;
304 }
305 return {
306 method,
307 body,
308 headers,
309 };
310}
311//# sourceMappingURL=resource.js.map
\No newline at end of file