Skip to content

Instantly share code, notes, and snippets.

@jaredcnance
Last active March 4, 2018 20:38
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jaredcnance/7ff6c8d72e7eacb19ee1c439c80c0692 to your computer and use it in GitHub Desktop.
import Identifiable from '../../models/Identifiable';
import Error from 'models/Error';
import Client from '../Client';
const { fetch } = global as any;
class Model implements Identifiable {
id: number;
name: string = 'name';
}
const model = new Model();
describe('Client', () => {
it('can fetch all', async () => {
// arrange
expect.assertions(2);
fetch.mockResponseOnce(JSON.stringify([model]));
// act
let response = await Client.findAll<Model>('models');
// assert
expect(response.length).toEqual(1);
expect(response[0].name).toEqual(model.name);
});
it('throws if http client error', async () => {
// arrange
expect.assertions(1);
fetch.mockResponseOnce('{}', { status: 400 });
try {
// act
await Client.findAll<Model>('models');
} catch (e) {
// assert
expect(e).toEqual(
new Error(400, `Received HTTP 400 response from server.`),
);
}
});
});
import Identifiable from '../models/Identifiable';
import Error from 'models/Error';
const { fetch } = window;
const route = `${process.env.REACT_APP_API_HOST}/${process.env
.REACT_APP_API_NAMESPACE}`;
const headers = {
accept: 'application/json',
};
export default class Client {
static async find<T extends Identifiable>(
resource: string,
id: number,
): Promise<T> {
const response = await fetch(`${route}/${resource}/${id}`, { headers });
if (!response.ok) {
throw new Error(
response.status,
`Received HTTP ${response.status} response from server.`,
);
}
let result = await response.json();
return result as T;
}
static async findAll<T extends Identifiable>(
resource: string,
): Promise<Array<T>> {
const response = await fetch(`${route}/${resource}`, { headers });
if (!response.ok) {
throw new Error(
response.status,
`Received HTTP ${response.status} response from server.`,
);
}
let result = await response.json();
return result as Array<T>;
}
}
import Identifiable from '../../models/Identifiable';
import Error from 'models/Error';
import ResourceStore from 'services/ResourceStore';
const { fetch } = global as any;
class Model implements Identifiable {
id: number = 1;
}
const model = new Model();
describe('ResourceStore', () => {
it('calls fetch on single getAll', async () => {
// arrange
expect.assertions(2);
fetch.resetMocks();
fetch.mockResponse(JSON.stringify([model]));
let store = new ResourceStore<Model>('resources');
// act
let response = await store.getAll();
// assert
expect(response.length).toEqual(1);
expect(fetch.mock.calls.length).toEqual(1);
});
it('calls fetch for each getAll', async () => {
// arrange
expect.assertions(1);
fetch.resetMocks();
fetch.mockResponse(JSON.stringify([model]));
let store = new ResourceStore<Model>('resources');
// act
await store.getAll();
await store.getAll();
// assert
expect(fetch.mock.calls.length).toEqual(2);
});
it('get does not call fetch if already loaded by getAll()', async () => {
// arrange
expect.assertions(1);
fetch.resetMocks();
fetch.mockResponse(JSON.stringify([model]));
let store = new ResourceStore<Model>('resources');
// act
await store.getAll();
await store.get(model.id);
expect(fetch.mock.calls.length).toEqual(1);
});
it('throws if http client error', async () => {
// arrange
expect.assertions(1);
fetch.resetMocks();
fetch.mockResponse('{}', { status: 400 });
try {
// act
let store = new ResourceStore<Model>('resources');
await store.getAll();
} catch (e) {
// assert
expect(e).toEqual(
new Error(400, `Received HTTP 400 response from server.`),
);
}
});
});
import Identifiable from '../models/Identifiable';
import Client from 'services/Client';
/**
* Used for reducing HTTP requests when a resource may have already been loaded
* into memory. The common scenario is when we need a collection via `/resources`
* and then shortly after need an individual resource `/resources/:id`
* Typically, this store should be re-instantiated by the parent route `/resources`.
* Expected behavior is documented by the unit tests.
*/
export default class ResourceStore<T extends Identifiable> {
_resourceName: string;
_resources: T[];
constructor(resourceName: string) {
this._resourceName = resourceName;
}
/**
* Get the resource by its id. If the resource has already been loaded
* by this store as a member of the collection, that instance will be
* returned. If the instance cannot be found (e.g. due to a page reload
* on /resources/:id) the instance will be requested from the web API.
*
* @param {number} id
* @returns {Promise<T>}
*/
async get(id: number): Promise<T> {
if (this._resources && this._resources.length > 0) {
let resource = this._resources.find((r: Identifiable) => r.id === id);
if (resource) {
return Promise.resolve<T>(resource);
}
}
return await Client.find<T>(this._resourceName, id);
}
/**
* Requests all resources from the web API and updates the store contents.
*
* @returns {Promise<T[]>}
* @memberof ResourceStore
*/
async getAll(): Promise<T[]> {
this._resources = await Client.findAll<T>(this._resourceName);
return this._resources;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment