Last active
March 4, 2018 20:38
-
-
Save jaredcnance/7ff6c8d72e7eacb19ee1c439c80c0692 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`), | |
); | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`), | |
); | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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