Skip to content

Commit

Permalink
Merge pull request #24 from metsavaht/feature/add-local-config-support
Browse files Browse the repository at this point in the history
Add `requestConfig` to better handle server-side rendering
  • Loading branch information
jorgenader authored Oct 30, 2017
2 parents 31d891f + 06e72d1 commit 5719911
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ coverage
npm-debug.log
es
.vscode
.idea/
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ endpoints. It's still possible to use Resources without a router(see [Resource a
- ``headers`` *(Object|Function: Object)*: Optional Function or Object which can be used to add any additional headers to requests.
- ``cookies`` *(Object|Function)*: Optional Function or Object which can be used to add any additional cookies to requests. Please note
that in modern browsers this is disabled due to security concerns.
- ``mutateResponse`` *(Function)*: Optional function with signature `(responseData, rawResponse: ResponseWrapper, resource: Resource) => responseData`
- ``mutateResponse`` *(Function)*: Optional function with signature `(responseData, rawResponse: ResponseWrapper, resource: Resource, requestConfig: Object) => responseData`
which can be used to mutate response data before resolving it. E.g. This can be used to provide access to raw
response codes and headers to your success handler.
- ``mutateError`` *(Function)*: Optional function with signature `(error: BaseResourceError, rawResponse: ResponseWrapper, resource: Resource) => error`
- ``mutateError`` *(Function)*: Optional function with signature `(error: BaseResourceError, rawResponse: ResponseWrapper, resource: Resource, requestConfig: Object) => error`
which can be used to mutate errors before rejecting them. E.g. This can be used to provide access to raw response codes
and headers to your error handler.
- ``statusSuccess`` *(Array[int])*: Array (or a single value) of status codes to treat as a success. Default: [200, 201, 204]
Expand All @@ -78,9 +78,10 @@ endpoints. It's still possible to use Resources without a router(see [Resource a
errors into a ValidationError object. The default handler is built for Django/DRF errors.
- ``prepareError`` *(Function)*: Function with signature `(err, parentConfig) => mixed` which is used to normalize a single error. The default
handler is built for Django/DRF errors.
- ``mutateRawResponse`` *(Function)*: **Advanced usage:** Optional function with signature `rawResponse: ResponseWrapper => rawResponse` which can be
- ``mutateRawResponse`` *(Function)*: **Advanced usage:** Optional function with signature `(rawResponse: ResponseWrapper, requestConfig: Object) => rawResponse` which can be
used to mutate the response before it is resolved to `responseData` or a `BaseResourceError` subclass. Use the
source of `ResponseWrapper`, `SuperagentResponse` and `GenericResource::ensureStatusAndJson` for guidance.
- ``withCredentials`` *(bool)*: Allow request backend to send cookies/authentication headers, useful when using same API for server-side rendering.

## Error handling

Expand Down Expand Up @@ -170,11 +171,12 @@ Do a get request to the resource endpoint with optional kwargs and query paramet

1. `kwargs={}` *(Object)*: Object containing the replacement values if the resource uses tokenized urls
2. `query={}` *(Object|string)*: Query parameters to use when doing the request.
3. `method='get'` *(string)*: Lowercase name of the HTTP method that will be used for this request.
3. `requestConfig=null` *(Object)*: Configuration overrides, useful when using same API for server-side rendering.
4. `method='get'` *(string)*: Lowercase name of the HTTP method that will be used for this request.

### ``Resource.options``

Alias for `Resource.fetch(kwargs, query, 'options')`
Alias for `Resource.fetch(kwargs, query, requestConfig, 'options')`

#### Returns

Expand All @@ -189,22 +191,23 @@ Do a `method` request to the resource endpoint with optional kwargs and query pa
1. `kwargs={}` *(Object)*: Object containing the replacement values if the resource uses tokenized urls
2. `data={}` *(Object|string)*: Query parameters to use when doing the request.
3. `query={}` *(Object|string)*: Query parameters to use when doing the request.
4. `method='post'` *(string)*: Lowercase name of the HTTP method that will be used for this request.
4. `requestConfig=null` *(Object)*: Configuration overrides, useful when using same API for server-side rendering.
5. `method='post'` *(string)*: Lowercase name of the HTTP method that will be used for this request.

#### Returns
*(Promise)*: Returns a `Promise` that resolves to the remote result or throws if errors occur.

### ``Resource.patch``

Alias for `Resource.post(kwargs, data, query, 'patch')`
Alias for `Resource.post(kwargs, data, query, requestConfig, 'patch')`

### ``Resource.put``

Alias for `Resource.post(kwargs, data, query, 'put')`
Alias for `Resource.post(kwargs, data, query, requestConfig, 'put')`

### ``Resource.del``

Alias for `Resource.post(kwargs, data, query, 'del')`
Alias for `Resource.post(kwargs, data, query, requestConfig, 'del')`

### ``BaseResourceError``

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tg-resources",
"version": "2.0.0-alpha.6",
"version": "2.0.0-alpha.7",
"description": "Abstractions on-top of `superagent` (or other Ajax libaries) for communication with REST.",
"main": "./dist/index.js",
"jsnext:main": "./es/index.js",
Expand Down
71 changes: 42 additions & 29 deletions src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,70 +33,79 @@ class GenericResource {
return !!this._parent || !!this._config;
}

get config() {
config(requestConfig = null) {
if (!this._config) {
this._config = mergeConfig(
this.parent ? this.parent.config : DEFAULTS,
this.parent ? this.parent.config() : DEFAULTS,
this._customConfig,
);
}

if (requestConfig && isObject(requestConfig)) {
return mergeConfig(this._config, requestConfig);
}

return this._config;
}

mutateRawResponse(rawResponse) {
if (isFunction(this.config.mutateRawResponse)) {
return this.config.mutateRawResponse(rawResponse);
mutateRawResponse(rawResponse, requestConfig) {
const config = this.config(requestConfig);
if (isFunction(config.mutateRawResponse)) {
return config.mutateRawResponse(rawResponse, requestConfig);
}

return rawResponse;
}

mutateResponse(responseData, rawResponse) {
if (isFunction(this.config.mutateResponse)) {
return this.config.mutateResponse(responseData, rawResponse, this);
mutateResponse(responseData, rawResponse, requestConfig) {
const config = this.config(requestConfig);
if (isFunction(config.mutateResponse)) {
return config.mutateResponse(responseData, rawResponse, this, requestConfig);
}

return responseData;
}

mutateError(error, rawResponse) {
if (isFunction(this.config.mutateError)) {
return this.config.mutateError(error, rawResponse, this);
mutateError(error, rawResponse, requestConfig) {
const config = this.config(requestConfig);
if (isFunction(config.mutateError)) {
return config.mutateError(error, rawResponse, this, requestConfig);
}

return error;
}

getHeaders() {
getHeaders(requestConfig = null) {
const config = this.config(requestConfig);
const headers = {
...(this.parent ? this.parent.getHeaders() : {}),
...((isFunction(this.config.headers) ? this.config.headers() : this.config.headers) || {}),
...((isFunction(config.headers) ? config.headers() : config.headers) || {}),
};

const cookieVal = serializeCookies(this.getCookies());
const cookieVal = serializeCookies(this.getCookies(requestConfig));
if (cookieVal) {
headers.Cookie = cookieVal;
}

// if Accept is null/undefined, add default accept header automatically (backwards incompatible for text/html)
if (!hasValue(headers.Accept)) {
headers.Accept = this.config.defaultAcceptHeader;
headers.Accept = config.defaultAcceptHeader;
}

return headers;
}

getCookies() {
getCookies(requestConfig = null) {
const config = this.config(requestConfig);
return {
...(this.parent ? this.parent.getCookies() : {}),
...((isFunction(this.config.cookies) ? this.config.cookies() : this.config.cookies) || {}),
...((isFunction(config.cookies) ? config.cookies() : config.cookies) || {}),
};
}

handleRequest(req) {
handleRequest(req, requestConfig) {
return this.ensureStatusAndJson(new Promise((resolve) => {
const headers = this.getHeaders();
const headers = this.getHeaders(requestConfig);

if (headers && isObject(headers)) {
Object.keys(headers).forEach((key) => {
Expand All @@ -107,29 +116,32 @@ class GenericResource {
}

this.doRequest(req, (response, error) => resolve(this.constructor.wrapResponse(response, error, req)));
}));
}), requestConfig);
}

ensureStatusAndJson(prom) {
ensureStatusAndJson(prom, requestConfig) {
const config = this.config(requestConfig);
return prom.then((origRes) => {
const res = this.mutateRawResponse(origRes);
const res = this.mutateRawResponse(origRes, requestConfig);

// If no error occured
if (res && !res.hasError) {
if (this.config.statusSuccess.indexOf(res.status) !== -1) {
if (config.statusSuccess.indexOf(res.status) !== -1) {
// Got statusSuccess response code, lets resolve this promise
return this.mutateResponse(res.data, res);
} else if (this.config.statusValidationError.indexOf(res.status) !== -1) {
return this.mutateResponse(res.data, res, requestConfig);
} else if (config.statusValidationError.indexOf(res.status) !== -1) {
// Got statusValidationError response code, lets throw RequestValidationError
throw this.mutateError(
new RequestValidationError(res.status, res.text, this.config),
new RequestValidationError(res.status, res.text, config),
res,
requestConfig,
);
} else {
// Throw a InvalidResponseCode error
throw this.mutateError(
new InvalidResponseCode(res.status, res.text),
res,
requestConfig,
);
}
} else {
Expand All @@ -144,14 +156,15 @@ class GenericResource {
});
}

buildThePath(urlParams) {
buildThePath(urlParams, requestConfig) {
let thePath = this.apiEndpoint;
const config = this.config(requestConfig);

if (urlParams && !(isObject(urlParams) && Object.keys(urlParams).length === 0)) {
thePath = renderTemplate(this.apiEndpoint)(urlParams);
}

return `${this.config.apiRoot}${thePath}`;
return `${config.apiRoot}${thePath}`;
}

/* istanbul ignore next */
Expand All @@ -160,7 +173,7 @@ class GenericResource {
}

/* istanbul ignore next */
createRequest(method, url, query, data) { // eslint-disable-line class-methods-use-this, no-unused-vars
createRequest(method, url, query, data, requestConfig) { // eslint-disable-line class-methods-use-this, no-unused-vars
throw new Error('Not implemented');
}

Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const DEFAULTS = {
mutateRawResponse: null,
headers: null,
cookies: null,
withCredentials: false,

parseErrors,
prepareError,
Expand Down
8 changes: 4 additions & 4 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Router {
getHeaders() {
const headers = {
...(this.parent ? this.parent.getHeaders() : {}),
...((isFunction(this.config.headers) ? this.config.headers() : this.config.headers) || {}),
...((isFunction(this.config().headers) ? this.config().headers() : this.config().headers) || {}),
};

return headers;
Expand All @@ -35,7 +35,7 @@ class Router {
getCookies() {
return {
...(this.parent ? this.parent.getCookies() : {}),
...((isFunction(this.config.cookies) ? this.config.cookies() : this.config.cookies) || {}),
...((isFunction(this.config().cookies) ? this.config().cookies() : this.config().cookies) || {}),
};
}

Expand All @@ -51,10 +51,10 @@ class Router {
return !!this._parent || !!this._config;
}

get config() {
config() {
if (!this._config) {
this._config = mergeConfig(
this._parent ? this._parent.config : DEFAULTS,
this._parent ? this._parent.config() : DEFAULTS,
this.defaultConfig || this.constructor.defaultConfig || null,
this._customConfig,
);
Expand Down
34 changes: 18 additions & 16 deletions src/single.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
export default function makeSingle(baseClass) {
class SingleObjectResource extends baseClass {
fetch(kwargs, query, method = 'get') {
const thePath = this.buildThePath(kwargs);
return this.handleRequest(this.createRequest(method, thePath, query));
fetch(kwargs, query, requestConfig = null, method = 'get') {
const thePath = this.buildThePath(kwargs, requestConfig);
return this.handleRequest(this.createRequest(method, thePath, query, null, requestConfig), requestConfig);
}

head(kwargs, query) {
return this.fetch(kwargs, query, 'head');
head(kwargs, query, requestConfig = null) {
return this.fetch(kwargs, query, requestConfig, 'head');
}

options(kwargs, query) {
return this.fetch(kwargs, query, 'options');
options(kwargs, query, requestConfig = null) {
return this.fetch(kwargs, query, requestConfig, 'options');
}

post(kwargs, data, query, /* istanbul ignore next: https://github.com/istanbuljs/babel-plugin-istanbul/issues/94 */ method = 'post') {
const thePath = this.buildThePath(kwargs);
post(kwargs, data, query, requestConfig = null, /* istanbul ignore next: https://github.com/istanbuljs/babel-plugin-istanbul/issues/94 */ method = 'post') {
const thePath = this.buildThePath(kwargs, requestConfig);

return this.handleRequest(this.createRequest(method, thePath, query, data || {}));
return this.handleRequest(
this.createRequest(method, thePath, query, data || {}, requestConfig), requestConfig,
);
}

patch(kwargs, data, query) {
return this.post(kwargs, data, query, 'patch');
patch(kwargs, data, query, requestConfig = null) {
return this.post(kwargs, data, query, requestConfig, 'patch');
}

put(kwargs, data, query) {
return this.post(kwargs, data, query, 'put');
put(kwargs, data, query, requestConfig = null) {
return this.post(kwargs, data, query, requestConfig, 'put');
}

del(kwargs, data, query) {
return this.post(kwargs, data, query, 'del');
del(kwargs, data, query, requestConfig = null) {
return this.post(kwargs, data, query, requestConfig, 'del');
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/superagent/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ export class SuperAgentResource extends GenericResource {
return new SuperagentResponse(response, error && error.status === undefined ? error : null);
}

createRequest(method, url, query, data) { // eslint-disable-line class-methods-use-this
createRequest(method, url, query, data, requestConfig) { // eslint-disable-line class-methods-use-this
method = method.toLowerCase();

let req = request[method](url);

if (this.config(requestConfig).withCredentials) {
req = req.withCredentials();
}

if (query) {
req = req.query(query);
}
Expand Down
5 changes: 5 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export function bindResources(routes, $this) {
throw new Error(`Route '${routeName}' is invalid. Route names must not start with an underscore`);
}

if (routeName === 'config') {
throw new Error(`Route ${routeName} collides with Router built-in method names`);
}


if (routes[routeName].isBound) {
throw new Error(`Route '${routeName}' is bound already`);
}
Expand Down
Loading

0 comments on commit 5719911

Please sign in to comment.