ketting
Version:
Opinionated HATEOAS / Rest client.
121 lines • 3.89 kB
JavaScript
import qs from 'query-string';
export class SimpleAction {
/**
* What url to post the form to.
*/
uri;
/**
* Action name.
*
* Some formats call this the 'rel'
*/
name;
/**
* Form title.
*
* Should be human-friendly.
*/
title;
/**
* The HTTP method to use
*/
method;
/**
* The contentType to use for the form submission
*/
contentType;
/**
* Returns the list of fields associated to an action
*/
fields;
/**
* Reference to client
*/
client;
constructor(client, formInfo) {
this.client = client;
for (const [k, v] of Object.entries(formInfo)) {
this[k] = v;
}
}
/**
* Execute the action or submit the form.
*/
async submit(formData) {
const uri = new URL(this.uri);
const newFormData = this.validateForm(formData);
if (this.method === 'GET') {
uri.search = qs.stringify(newFormData);
const resource = this.client.go(uri.toString());
return resource.get();
}
const response = await this.fetchOrThrowWithBody(uri, newFormData);
const state = this.client.getStateForResponse(uri.toString(), response);
return state;
}
async submitFollow(formData) {
const uri = new URL(this.uri);
const newFormData = this.validateForm(formData);
if (this.method === 'GET') {
uri.search = qs.stringify(newFormData);
return this.client.go(uri.toString());
}
const response = await this.fetchOrThrowWithBody(uri, newFormData);
switch (response.status) {
case 201:
if (response.headers.has('location')) {
return this.client.go(response.headers.get('location'));
}
throw new Error('Could not follow after a 201 request, because the server did not reply with a Location header. If you sent a Location header, check if your service is returning "Access-Control-Expose-Headers: Location".');
case 204:
case 205:
return this.client.go(uri.toString());
default:
throw new Error('Did not receive a 201, 204 or 205 status code so we could not follow to the next resource');
}
}
validateForm(formData) {
const newFormData = {
...formData
};
for (const field of this.fields) {
if (!(field.name in formData)) {
if (field.value) {
// We don't have perfect types for fields vs. FormData and how they
// related, so 'any' is needed here.
newFormData[field.name] = field.value;
}
else if (field.required) {
throw new Error(`The ${field.name} field is required in this form`);
}
}
}
return newFormData;
}
fetchOrThrowWithBody(uri, formData) {
let body;
switch (this.contentType) {
case 'application/x-www-form-urlencoded':
body = qs.stringify(formData);
break;
case 'application/json':
body = JSON.stringify(formData);
break;
default:
throw new Error(`Serializing mimetype ${this.contentType} is not yet supported in actions`);
}
return this.client.fetcher.fetchOrThrow(uri.toString(), {
method: this.method,
body,
headers: {
'Content-Type': this.contentType
}
});
}
field(name) {
return this.fields.find(field => field.name === name);
}
}
export class ActionNotFound extends Error {
}
//# sourceMappingURL=action.js.map