From c25a0944a959c07c7530c843484408caf64541c5 Mon Sep 17 00:00:00 2001 From: Nathan Lisgo Date: Thu, 23 Jan 2020 16:30:53 +0000 Subject: [PATCH] feat: allow individual article retrieval Generates a unique IRI for each article, and allows them to be accessed individually. --- src/router.ts | 2 ++ src/routes/add-article.ts | 4 +-- src/routes/article.ts | 28 ++++++++++++++++ src/routes/index.ts | 1 + test/routes/add-article.test.ts | 2 +- test/routes/article.test.ts | 59 +++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/routes/article.ts create mode 100644 test/routes/article.test.ts diff --git a/src/router.ts b/src/router.ts index 585ed62b..9fd2b7fb 100644 --- a/src/router.ts +++ b/src/router.ts @@ -3,6 +3,7 @@ import { AppServiceContext, AppState } from './app'; import Routes from './routes'; import addArticle from './routes/add-article'; import apiDocumentation from './routes/api-documentation'; +import article from './routes/article'; import articleList from './routes/article-list'; import entryPoint from './routes/entry-point'; @@ -12,6 +13,7 @@ export default (): Router => { router.get(Routes.ApiDocumentation, '/doc', apiDocumentation()); router.get(Routes.ArticleList, '/articles', articleList()); router.post(Routes.AddArticle, '/articles', addArticle()); + router.get(Routes.Article, '/articles/:id', article()); router.get(Routes.EntryPoint, '/', entryPoint()); return router; diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 5d788b0d..18901032 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -28,7 +28,7 @@ export default (): AppMiddleware => ( throw new createHttpError.BadRequest(`Article must have at least one ${termToString(schema('name'))}`); } - const newId = namedNode(uniqueString()); + const newId = namedNode(url.resolve(request.origin, router.url(Routes.Article, uniqueString()))); [...request.dataset].forEach((originalQuad: Quad): void => { let newQuad: Quad; @@ -46,7 +46,7 @@ export default (): AppMiddleware => ( await articles.set(newId, request.dataset); response.status = CREATED; - response.set('Location', url.resolve(request.origin, router.url(Routes.ArticleList))); + response.set('Location', newId.value); await next(); } diff --git a/src/routes/article.ts b/src/routes/article.ts new file mode 100644 index 00000000..34c421c5 --- /dev/null +++ b/src/routes/article.ts @@ -0,0 +1,28 @@ +import createHttpError from 'http-errors'; +import { OK } from 'http-status-codes'; +import { Next } from 'koa'; +import url from 'url'; +import { AppContext, AppMiddleware } from '../app'; +import ArticleNotFound from '../errors/article-not-found'; + +export default (): AppMiddleware => ( + async ({ + articles, dataFactory: { namedNode }, request, response, + }: AppContext, next: Next): Promise => { + const id = namedNode(url.resolve(request.origin, request.path)); + + try { + response.dataset = await articles.get(id); + } catch (error) { + if (error instanceof ArticleNotFound) { + throw new createHttpError.NotFound(error.message); + } + + throw error; + } + + response.status = OK; + + await next(); + } +); diff --git a/src/routes/index.ts b/src/routes/index.ts index 9f67ecaf..7089f0cb 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,7 @@ enum Routes { 'AddArticle' = 'add-article', 'ApiDocumentation' = 'api-documentation', + 'Article' = 'article', 'ArticleList' = 'article-list', 'EntryPoint' = 'entry-point', } diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index d1a659cb..0010a335 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -30,11 +30,11 @@ describe('add article', (): void => { const response = await makeRequest(createArticle({ id, name }), undefined, articles); expect(response.status).toBe(CREATED); - expect(response.get('Location')).toBe('http://example.com/path-to/article-list'); expect(await articles.count()).toBe(1); const [newId, dataset] = (await all(articles))[0]; + expect(response.get('Location')).toBe(newId.value); expect(dataset.has(quad(newId, schema('name'), name))).toBe(true); }); diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts new file mode 100644 index 00000000..7a94a0eb --- /dev/null +++ b/test/routes/article.test.ts @@ -0,0 +1,59 @@ +import { namedNode } from '@rdfjs/data-model'; +import createHttpError from 'http-errors'; +import { OK } from 'http-status-codes'; +import 'jest-rdf'; +import { Response } from 'koa'; +import InMemoryArticles from '../../src/adaptors/in-memory-articles'; +import Articles from '../../src/articles'; +import { WithDataset } from '../../src/middleware/dataset'; +import article from '../../src/routes/article'; +import createContext from '../context'; +import createArticle from '../create-article'; +import runMiddleware, { NextMiddleware } from '../middleware'; + +const makeRequest = async ( + path: string, articles?: Articles, next?: NextMiddleware, +): Promise> => ( + runMiddleware(article(), createContext({ articles, path }), next) +); + +describe('article', (): void => { + it('should return a successful response', async (): Promise => { + const id = namedNode('http://example.com/path-to/article/one'); + const articles = new InMemoryArticles(); + await articles.set(id, createArticle({ id })); + + const response = await makeRequest('path-to/article/one', articles); + + expect(response.status).toBe(OK); + }); + + it('should return the article', async (): Promise => { + const id = namedNode('http://example.com/path-to/article/one'); + const articles = new InMemoryArticles(); + const article1 = createArticle({ id }); + await articles.set(id, article1); + + const response = await makeRequest('path-to/article/one', articles); + + expect([...response.dataset]).toEqualRdfQuadArray([...article1]); + }); + + it('should throw an error if article is not found', async (): Promise => { + const response = makeRequest('path-to/article/not-found'); + + await expect(response).rejects.toBeInstanceOf(createHttpError.NotFound); + await expect(response).rejects.toHaveProperty('message', 'Article http://example.com/path-to/article/not-found could not be found'); + }); + + it('should call the next middleware', async (): Promise => { + const id = namedNode('http://example.com/path-to/article/one'); + const articles = new InMemoryArticles(); + await articles.set(id, createArticle({ id })); + const next = jest.fn(); + + await makeRequest('path-to/article/one', articles, next); + + expect(next).toHaveBeenCalledTimes(1); + }); +});